replicas-mcp 0.2.11 → 0.2.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1819 -888
- package/package.json +3 -2
- package/src/index.ts +1 -1
- package/tsconfig.json +4 -4
package/dist/index.js
CHANGED
|
@@ -3,24 +3,56 @@
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5
5
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
|
-
import { z } from "zod";
|
|
6
|
+
import { z as z3 } from "zod";
|
|
7
7
|
|
|
8
8
|
// ../shared/src/agent.ts
|
|
9
|
-
var
|
|
9
|
+
var AGENT = {
|
|
10
|
+
CLAUDE: "claude",
|
|
11
|
+
CODEX: "codex",
|
|
12
|
+
CURSOR: "cursor",
|
|
13
|
+
DEEPSEEK: "deepseek",
|
|
14
|
+
FX: "fx",
|
|
15
|
+
OPENCODE: "opencode",
|
|
16
|
+
PI: "pi",
|
|
17
|
+
RELAY: "relay"
|
|
18
|
+
};
|
|
19
|
+
var VALID_AGENT_PROVIDERS = Object.values(AGENT);
|
|
20
|
+
var VALID_RELAY_SUBAGENT_PROVIDERS = VALID_AGENT_PROVIDERS;
|
|
10
21
|
var VALID_CODING_AGENT_PROVIDERS = VALID_AGENT_PROVIDERS.filter(
|
|
11
|
-
(provider) => provider !==
|
|
22
|
+
(provider) => provider !== AGENT.RELAY
|
|
12
23
|
);
|
|
13
24
|
function isValidCodingAgentProvider(value) {
|
|
14
25
|
return VALID_CODING_AGENT_PROVIDERS.some((provider) => provider === value);
|
|
15
26
|
}
|
|
27
|
+
var VALID_THINKING_LEVELS = ["low", "medium", "high", "xhigh", "max", "ultra", "ultracode"];
|
|
28
|
+
var STANDARD_THINKING_LEVELS = ["low", "medium", "high", "xhigh", "max"];
|
|
29
|
+
var THINKING_LEVELS_BY_AGENT = {
|
|
30
|
+
claude: [...STANDARD_THINKING_LEVELS, "ultracode"],
|
|
31
|
+
codex: [...STANDARD_THINKING_LEVELS, "ultra"],
|
|
32
|
+
cursor: STANDARD_THINKING_LEVELS,
|
|
33
|
+
deepseek: STANDARD_THINKING_LEVELS,
|
|
34
|
+
fx: STANDARD_THINKING_LEVELS,
|
|
35
|
+
opencode: STANDARD_THINKING_LEVELS,
|
|
36
|
+
pi: STANDARD_THINKING_LEVELS,
|
|
37
|
+
relay: [...STANDARD_THINKING_LEVELS, "ultracode"]
|
|
38
|
+
};
|
|
39
|
+
function isValidThinkingLevel(value) {
|
|
40
|
+
return VALID_THINKING_LEVELS.some((l) => l === value);
|
|
41
|
+
}
|
|
16
42
|
function getProviderDisplayName(provider, variant = "short") {
|
|
17
43
|
switch (provider) {
|
|
18
44
|
case "codex":
|
|
19
45
|
return "Codex";
|
|
20
46
|
case "cursor":
|
|
21
47
|
return "Cursor";
|
|
48
|
+
case "deepseek":
|
|
49
|
+
return variant === "long" ? "DeepSeek Harness" : "DeepSeek";
|
|
50
|
+
case "fx":
|
|
51
|
+
return "fx";
|
|
22
52
|
case "opencode":
|
|
23
53
|
return "Opencode";
|
|
54
|
+
case "pi":
|
|
55
|
+
return "Pi";
|
|
24
56
|
case "relay":
|
|
25
57
|
return "Relay";
|
|
26
58
|
case "claude":
|
|
@@ -31,9 +63,141 @@ function getCodingAgentDisplayNames() {
|
|
|
31
63
|
return VALID_CODING_AGENT_PROVIDERS.map((provider) => getProviderDisplayName(provider)).join(", ");
|
|
32
64
|
}
|
|
33
65
|
|
|
66
|
+
// ../shared/src/analytics/types.ts
|
|
67
|
+
import { z } from "zod";
|
|
68
|
+
|
|
69
|
+
// ../shared/src/credentials/types.ts
|
|
70
|
+
var CREDENTIAL_SCOPE = {
|
|
71
|
+
USER: "user",
|
|
72
|
+
ORG: "org"
|
|
73
|
+
};
|
|
74
|
+
var CREDENTIAL_SCOPES = Object.values(CREDENTIAL_SCOPE);
|
|
75
|
+
var CREDENTIAL_SCOPE_PRIORITY = [CREDENTIAL_SCOPE.USER, CREDENTIAL_SCOPE.ORG];
|
|
76
|
+
|
|
77
|
+
// ../shared/src/type-guards.ts
|
|
78
|
+
function isRecord(value) {
|
|
79
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ../shared/src/credentials/agents.ts
|
|
83
|
+
var CREDENTIAL_METHOD = {
|
|
84
|
+
OAUTH: "oauth",
|
|
85
|
+
API_KEY: "api_key",
|
|
86
|
+
FOUNDRY: "foundry",
|
|
87
|
+
BEDROCK: "bedrock",
|
|
88
|
+
OPENCODE_GO: "opencode-go",
|
|
89
|
+
ASTER: "aster",
|
|
90
|
+
OPENROUTER: "openrouter",
|
|
91
|
+
AI_GATEWAY: "ai-gateway"
|
|
92
|
+
};
|
|
93
|
+
var CREDENTIAL_TYPE = {
|
|
94
|
+
CLAUDE_OAUTH: "claude",
|
|
95
|
+
ANTHROPIC_API_KEY: "claude_anthropic_api_key",
|
|
96
|
+
CLAUDE_FOUNDRY: "claude_foundry",
|
|
97
|
+
CLAUDE_BEDROCK: "claude_bedrock",
|
|
98
|
+
CODEX_OAUTH: "codex",
|
|
99
|
+
OPENAI_API_KEY: "openai_api_key",
|
|
100
|
+
CODEX_FOUNDRY: "codex_foundry",
|
|
101
|
+
CURSOR_API_KEY: "cursor_api_key",
|
|
102
|
+
OPENCODE_GO_API_KEY: "opencode_go_api_key",
|
|
103
|
+
ASTER_API_KEY: "aster_api_key",
|
|
104
|
+
OPENROUTER_API_KEY: "openrouter_api_key",
|
|
105
|
+
AI_GATEWAY_API_KEY: "ai_gateway_api_key"
|
|
106
|
+
};
|
|
107
|
+
var AGENT_CREDENTIAL_METHODS_IN_ORDER = {
|
|
108
|
+
[AGENT.CLAUDE]: [
|
|
109
|
+
{ method: CREDENTIAL_METHOD.OAUTH, credentialType: CREDENTIAL_TYPE.CLAUDE_OAUTH },
|
|
110
|
+
{ method: CREDENTIAL_METHOD.API_KEY, credentialType: CREDENTIAL_TYPE.ANTHROPIC_API_KEY },
|
|
111
|
+
{ method: CREDENTIAL_METHOD.FOUNDRY, credentialType: CREDENTIAL_TYPE.CLAUDE_FOUNDRY },
|
|
112
|
+
{ method: CREDENTIAL_METHOD.BEDROCK, credentialType: CREDENTIAL_TYPE.CLAUDE_BEDROCK }
|
|
113
|
+
],
|
|
114
|
+
[AGENT.CODEX]: [
|
|
115
|
+
{ method: CREDENTIAL_METHOD.OAUTH, credentialType: CREDENTIAL_TYPE.CODEX_OAUTH },
|
|
116
|
+
{ method: CREDENTIAL_METHOD.API_KEY, credentialType: CREDENTIAL_TYPE.OPENAI_API_KEY },
|
|
117
|
+
{ method: CREDENTIAL_METHOD.FOUNDRY, credentialType: CREDENTIAL_TYPE.CODEX_FOUNDRY }
|
|
118
|
+
],
|
|
119
|
+
[AGENT.CURSOR]: [
|
|
120
|
+
{ method: CREDENTIAL_METHOD.API_KEY, credentialType: CREDENTIAL_TYPE.CURSOR_API_KEY }
|
|
121
|
+
],
|
|
122
|
+
[AGENT.DEEPSEEK]: [
|
|
123
|
+
{ method: CREDENTIAL_METHOD.OPENROUTER, credentialType: CREDENTIAL_TYPE.OPENROUTER_API_KEY }
|
|
124
|
+
],
|
|
125
|
+
[AGENT.FX]: [
|
|
126
|
+
{ method: CREDENTIAL_METHOD.AI_GATEWAY, credentialType: CREDENTIAL_TYPE.AI_GATEWAY_API_KEY }
|
|
127
|
+
],
|
|
128
|
+
[AGENT.OPENCODE]: [
|
|
129
|
+
{ method: CREDENTIAL_METHOD.OPENCODE_GO, credentialType: CREDENTIAL_TYPE.OPENCODE_GO_API_KEY },
|
|
130
|
+
{ method: CREDENTIAL_METHOD.ASTER, credentialType: CREDENTIAL_TYPE.ASTER_API_KEY },
|
|
131
|
+
{ method: CREDENTIAL_METHOD.OPENROUTER, credentialType: CREDENTIAL_TYPE.OPENROUTER_API_KEY }
|
|
132
|
+
],
|
|
133
|
+
[AGENT.PI]: [
|
|
134
|
+
{ method: CREDENTIAL_METHOD.ASTER, credentialType: CREDENTIAL_TYPE.ASTER_API_KEY },
|
|
135
|
+
{ method: CREDENTIAL_METHOD.OPENROUTER, credentialType: CREDENTIAL_TYPE.OPENROUTER_API_KEY }
|
|
136
|
+
]
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
// ../shared/src/analytics/types.ts
|
|
140
|
+
var agentCredentialSnapshotSchema = z.object({
|
|
141
|
+
method: z.enum(CREDENTIAL_METHOD),
|
|
142
|
+
scope: z.enum(CREDENTIAL_SCOPE),
|
|
143
|
+
revision: z.string().optional()
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// ../shared/src/event.ts
|
|
147
|
+
var CLAUDE_PARTIAL_MESSAGE_EVENT_TYPE = "claude-partial-message";
|
|
148
|
+
function coerceClaudePartialMessagePayload(payload) {
|
|
149
|
+
const streamId = payload.streamId;
|
|
150
|
+
if (typeof streamId !== "string" || !streamId) return null;
|
|
151
|
+
const parentToolUseId = payload.parent_tool_use_id;
|
|
152
|
+
const text = typeof payload.text === "string" && payload.text ? payload.text : void 0;
|
|
153
|
+
const thinking = typeof payload.thinking === "string" && payload.thinking ? payload.thinking : void 0;
|
|
154
|
+
if (!text && !thinking) return null;
|
|
155
|
+
return {
|
|
156
|
+
streamId,
|
|
157
|
+
parent_tool_use_id: typeof parentToolUseId === "string" ? parentToolUseId : null,
|
|
158
|
+
...text ? { text } : {},
|
|
159
|
+
...thinking ? { thinking } : {},
|
|
160
|
+
status: payload.status === "completed" ? "completed" : "in_progress"
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
var USER_MESSAGE_ID_PAYLOAD_KEY = "replicasMessageId";
|
|
164
|
+
var CODEX_ASP_ITEM_ID_PAYLOAD_KEY = "codexAspItemId";
|
|
165
|
+
var CODEX_QUOTA_STATUS_EVENT_TYPE = "codex-quota-status";
|
|
166
|
+
|
|
167
|
+
// ../shared/src/urls.ts
|
|
168
|
+
var HOME_PATH = "/home";
|
|
169
|
+
var DASHBOARD_PATH = "/dashboard";
|
|
170
|
+
var WEB_APP_PATHS = {
|
|
171
|
+
home: HOME_PATH,
|
|
172
|
+
onboarding: `${HOME_PATH}/onboarding`,
|
|
173
|
+
workspaces: `${HOME_PATH}/workspace`,
|
|
174
|
+
automations: `${HOME_PATH}/automations`,
|
|
175
|
+
environment: `${HOME_PATH}/environment`,
|
|
176
|
+
analytics: `${HOME_PATH}/analytics`,
|
|
177
|
+
dashboard: DASHBOARD_PATH,
|
|
178
|
+
agents: `${DASHBOARD_PATH}/agents`,
|
|
179
|
+
integrations: `${DASHBOARD_PATH}/integrations`,
|
|
180
|
+
integrationsGithub: `${DASHBOARD_PATH}/integrations/github`,
|
|
181
|
+
integrationsGitlab: `${DASHBOARD_PATH}/integrations/gitlab`,
|
|
182
|
+
settings: `${DASHBOARD_PATH}/settings`,
|
|
183
|
+
billing: `${DASHBOARD_PATH}/billing`,
|
|
184
|
+
members: `${DASHBOARD_PATH}/members`,
|
|
185
|
+
"audit-logs": `${DASHBOARD_PATH}/audit-logs`,
|
|
186
|
+
accountAgents: `${DASHBOARD_PATH}/account/agents`,
|
|
187
|
+
accountProfile: `${DASHBOARD_PATH}/account/profile`,
|
|
188
|
+
accountIntegrations: `${DASHBOARD_PATH}/account/integrations`,
|
|
189
|
+
accountApiKeys: `${DASHBOARD_PATH}/account/api-keys`,
|
|
190
|
+
accountPreferences: `${DASHBOARD_PATH}/account/preferences`,
|
|
191
|
+
fullscreenWorkspaces: "/workspaces"
|
|
192
|
+
};
|
|
193
|
+
var LEGACY_WEB_APP_PATHS = {
|
|
194
|
+
app: "/app",
|
|
195
|
+
dashboardWorkspaces: `${DASHBOARD_PATH}/workspaces`
|
|
196
|
+
};
|
|
197
|
+
|
|
34
198
|
// ../shared/src/default-skills/replicas-agent/abilities/computer.ts
|
|
35
199
|
var SECTION = `### Computer use (Linux desktop control)
|
|
36
|
-
Drive the workspace's Linux desktop - open a browser, click, type, scroll, screenshot, record - via the \`replicas computer\` CLI. Every Replicas workspace boots with
|
|
200
|
+
Drive the workspace's Linux desktop - open a browser, click, type, scroll, screenshot, record - via the \`replicas computer\` CLI. Every Replicas workspace boots with a live H.264 and Opus stream published as an authenticated preview, so a \`Desktop\` tab is always available in the dashboard. Use \`replicas computer info\` to get the viewer URL when you want to share it (e.g. point a user on Slack at the live stream).
|
|
37
201
|
|
|
38
202
|
**Reference:** \`references/COMPUTER-USE.md\`
|
|
39
203
|
|
|
@@ -44,7 +208,7 @@ Use this when:
|
|
|
44
208
|
- You want to record a screen capture of a task as proof to share back`;
|
|
45
209
|
var REFERENCE = `# Computer use (Linux desktop control)
|
|
46
210
|
|
|
47
|
-
Every Replicas workspace boots with a full Linux desktop stack - Xvfb (1920\xD71080), openbox, tint2,
|
|
211
|
+
Every Replicas workspace boots with a full Linux desktop stack - Xvfb (1920\xD71080), openbox, tint2, PulseAudio, ffmpeg, and Google Chrome. The engine auto-registers the H.264 and Opus viewer (port 6080) as an authenticated preview at startup, so the dashboard \`Desktop\` tab is live the moment the workspace is up. You drive the desktop through the \`replicas computer\` CLI.
|
|
48
212
|
|
|
49
213
|
Use this for anything the user can't reasonably do via an API - clicking around web apps, filling forms, testing UI changes, dragging files between desktop apps, recording a walkthrough.
|
|
50
214
|
|
|
@@ -79,25 +243,24 @@ replicas computer info
|
|
|
79
243
|
# 2) Launch a browser on the workspace display.
|
|
80
244
|
replicas computer launch chrome https://news.ycombinator.com
|
|
81
245
|
|
|
82
|
-
# 3)
|
|
83
|
-
replicas computer
|
|
84
|
-
replicas computer browser --snapshot
|
|
246
|
+
# 3) Capture the settled viewport and native accessibility tree together.
|
|
247
|
+
replicas computer browser-state /tmp/state.png --full
|
|
85
248
|
|
|
86
|
-
# 4)
|
|
87
|
-
replicas computer
|
|
88
|
-
replicas computer
|
|
249
|
+
# 4) Batch actions that use refs from the same state, then capture the semantic diff.
|
|
250
|
+
replicas computer browser-batch '[{"action":"fill","ref":17,"value":"hello"},{"action":"click","ref":42}]'
|
|
251
|
+
replicas computer browser-state /tmp/after-click.png
|
|
89
252
|
|
|
90
253
|
# 5) (Optional) Record a screencap to share back.
|
|
91
254
|
replicas computer record start /tmp/demo.mp4 --fps 60
|
|
92
255
|
# ... do stuff ...
|
|
93
256
|
replicas computer record stop
|
|
94
|
-
replicas media upload /tmp/demo.mp4
|
|
257
|
+
replicas media upload /tmp/demo.mp4 --name "Demo recording"
|
|
95
258
|
\`\`\`
|
|
96
259
|
|
|
97
260
|
## Command reference
|
|
98
261
|
|
|
99
262
|
### \`replicas computer info\`
|
|
100
|
-
Prints the live
|
|
263
|
+
Prints the live desktop viewer URL (\`https://6080-<hash>.tryreplicas.com/\`). The engine auto-registers this authenticated preview on startup; \`info\` just looks it up. Use it when you want to share the live stream URL with the user out-of-band (e.g. in a Slack reply) - the dashboard \`Desktop\` tab already points at the same URL automatically.
|
|
101
264
|
|
|
102
265
|
If invoked very early in the workspace lifecycle, \`info\` will poll briefly while the engine finishes registering the preview, then error if it's still not available.
|
|
103
266
|
|
|
@@ -114,21 +277,33 @@ Waits briefly for the screen to stop changing, saves a 1:1 screenshot, and print
|
|
|
114
277
|
|
|
115
278
|
Use this instead of hand-written \`sleep && screenshot\` loops after clicks, navigation, typing, or page loads. By default it saves a 100px coordinate-grid screenshot and waits up to 3s for 600ms of visual stability. Pass \`--raw\` if you need an unannotated 1:1 screenshot.
|
|
116
279
|
|
|
117
|
-
### \`replicas computer browser\`
|
|
118
|
-
|
|
280
|
+
### \`replicas computer browser-state <path> [--full] [--timeout MS] [--stable-ms MS]\`
|
|
281
|
+
Waits for the selected Chrome page to settle, saves its viewport screenshot, and prints a compact native Chrome accessibility tree in the same coordinate space. Actionable nodes have DOM-backed refs such as \`[ref=42] button "Save"\`; the tree includes accessible roles, names, values, states, visible bounds, and iframe content.
|
|
282
|
+
|
|
283
|
+
The first call returns a full tree. Later calls for the same target and document return semantic changes; navigation or reload forces a new full tree. Pass \`--full\` to reset the baseline. Refs are valid for the current document.
|
|
284
|
+
|
|
285
|
+
### \`replicas computer browser [--snapshot]\`
|
|
286
|
+
Lists Chrome tabs. \`--snapshot\` returns the compatibility accessibility snapshot. Use \`browser-state\` for the primary action/observe loop.
|
|
287
|
+
|
|
288
|
+
### \`replicas computer browser-batch '<actions-json>'\`
|
|
289
|
+
Runs up to 100 ordered browser actions through one Chrome session and stops at the first failure. Supported actions are \`click\`, \`fill\`, \`key\`, \`type\`, \`scroll\`, and \`wait\`. Prefer this when several actions use refs from the same \`browser-state\`; it avoids repeated process, tab-selection, and connection setup. Do not carry refs across navigation\u2014capture fresh state and start a new batch.
|
|
290
|
+
|
|
291
|
+
### \`replicas computer browser-click --ref ID [--button left|middle|right] [--double]\`
|
|
292
|
+
Scrolls the referenced node into view and sends trusted Chrome mouse input at its current visible bounds. Prefer refs from \`browser-state\`. \`browser-click <text> [--exact] [--index N]\` remains a fallback when no fresh ref is available.
|
|
119
293
|
|
|
120
|
-
|
|
294
|
+
### \`replicas computer browser-fill --ref ID <value>\`
|
|
295
|
+
Focuses the referenced field, fills it with trusted Chrome input, and verifies the resulting value. Select controls use their semantic option value. \`browser-fill <field> <value> [--exact] [--index N]\` remains a text-matching fallback.
|
|
121
296
|
|
|
122
|
-
### \`replicas computer browser-
|
|
123
|
-
|
|
297
|
+
### \`replicas computer browser-scroll <up|down|left|right> [--ref ID] [--amount PX]\`
|
|
298
|
+
Sends trusted wheel input at a referenced element or the center of the viewport.
|
|
124
299
|
|
|
125
|
-
### \`replicas computer browser-
|
|
126
|
-
|
|
300
|
+
### \`replicas computer browser-key <combo>\` / \`browser-type <text>\`
|
|
301
|
+
Sends trusted keyboard input to the focused element in the selected Chrome target. Prefer \`browser-fill\` when setting a known field value.
|
|
127
302
|
|
|
128
303
|
### \`replicas computer browser-wait <text> [--mode any|text|title|url|control] [--exact] [--timeout MS]\`
|
|
129
304
|
Waits until the active Chrome page matches text in the title, URL, body text, or visible controls. Use this after \`browser-click\` / \`browser-fill\` when you need web app state to settle without screenshot polling.
|
|
130
305
|
|
|
131
|
-
For
|
|
306
|
+
For browser state and actions, pass \`--target-id <id>\`, \`--title <text>\`, \`--url <text>\`, or \`--page <n>\` when multiple Chrome tabs are open. Prefer \`--target-id\` because titles and URLs can change after an action. The matched tab is brought to the foreground before each action, so driving multiple tabs or recording the screen switches which tab is visible.
|
|
132
307
|
|
|
133
308
|
### \`replicas computer click <x> <y> [--button N] [--double] [--modifiers ctrl+shift]\`
|
|
134
309
|
Move to (x, y) and click. Coordinates can be absolute pixels or percentages such as \`50%\` \`50%\`. Default is left-click (button 1); pass \`--button 3\` for right-click. \`--modifiers\` holds keys during the click (e.g. ctrl-click a link to open in a new tab).
|
|
@@ -161,26 +336,29 @@ Spawns an app on the workspace display. Built-in aliases:
|
|
|
161
336
|
Anything else gets \`exec\`'d verbatim, so \`replicas computer launch xeyes\` works if xeyes is installed.
|
|
162
337
|
When opening a known page, prefer passing the URL directly: \`replicas computer launch chrome http://localhost:3000/\`.
|
|
163
338
|
|
|
339
|
+
Chrome launches do not return until the requested page is controllable. The output includes its page ID; carry that ID with \`--target-id\` into \`browser-state\` and browser actions so unrelated tabs cannot steal task context.
|
|
340
|
+
|
|
164
341
|
### \`replicas computer record start <path> [--fps N]\`
|
|
165
|
-
Starts an ffmpeg screen recorder. Output is a fragmented MP4 (still playable if the workspace dies mid-record). Default 60fps; drop to 30 if the workspace is CPU-constrained.
|
|
342
|
+
Starts an ffmpeg screen recorder and returns only after ffmpeg has written the first bytes. Output is post-processed when stopped: action windows stay at normal speed, idle gaps accelerate, click moments get eased camera zoom, and a synthetic cursor animates between logged mouse positions. The raw capture is fragmented MP4 while active (still playable if the workspace dies mid-record). Default 60fps; drop to 30 if the workspace is CPU-constrained.
|
|
166
343
|
|
|
167
344
|
Only one recording at a time. Re-running \`start\` while one is active fails - call \`stop\` first.
|
|
168
345
|
|
|
169
346
|
### \`replicas computer record stop\`
|
|
170
|
-
SIGINTs ffmpeg,
|
|
347
|
+
SIGINTs ffmpeg, verifies that the process has finalized the MP4, then prints the output path. A finalization timeout preserves the recording state so \`stop\` can be retried safely. Upload it with \`replicas media upload <path> --name "<name>"\` to share it.
|
|
171
348
|
|
|
172
349
|
## Patterns
|
|
173
350
|
|
|
174
|
-
###
|
|
175
|
-
|
|
351
|
+
### Browser action / state loop
|
|
352
|
+
For Chrome, use the native accessibility state before interpreting pixels or guessing coordinates:
|
|
176
353
|
|
|
177
354
|
\`\`\`bash
|
|
178
|
-
replicas computer
|
|
179
|
-
replicas computer
|
|
180
|
-
|
|
355
|
+
replicas computer launch chrome https://example.com
|
|
356
|
+
replicas computer browser-state /tmp/before.png --full --target-id <page-id>
|
|
357
|
+
replicas computer browser-batch '[{"action":"fill","ref":17,"value":"hello"},{"action":"click","ref":23}]' --target-id <page-id>
|
|
358
|
+
replicas computer browser-state /tmp/after.png --target-id <page-id>
|
|
181
359
|
\`\`\`
|
|
182
360
|
|
|
183
|
-
|
|
361
|
+
Read the screenshot and tree as one state. Batch ordered actions whose refs all come from that state, then inspect the next semantic diff. A batch is fail-fast and validates each ref against the cached document. Split at navigation or whenever a later action depends on newly rendered refs. Fall back to \`observe\` plus desktop coordinates for canvas content, browser chrome, non-Chrome apps, or controls missing from the accessibility tree.
|
|
184
362
|
|
|
185
363
|
### Typing into an address bar
|
|
186
364
|
\`\`\`bash
|
|
@@ -189,10 +367,9 @@ replicas computer observe /tmp/browser-open.png
|
|
|
189
367
|
replicas computer key ctrl+l
|
|
190
368
|
replicas computer type "https://example.com"
|
|
191
369
|
replicas computer key Return
|
|
192
|
-
replicas computer
|
|
193
|
-
replicas computer browser --
|
|
194
|
-
replicas computer browser-
|
|
195
|
-
replicas computer browser-click "More information" --title "Example"
|
|
370
|
+
replicas computer browser-state /tmp/loaded.png --full
|
|
371
|
+
replicas computer browser-fill --ref 17 "replicas"
|
|
372
|
+
replicas computer browser-click --ref 23 --title "Example"
|
|
196
373
|
replicas computer browser-wait "Example Domain" --mode title --title "Example"
|
|
197
374
|
\`\`\`
|
|
198
375
|
|
|
@@ -212,12 +389,13 @@ For tasks the user wants proof of:
|
|
|
212
389
|
replicas computer record start /tmp/walkthrough.mp4
|
|
213
390
|
# ... your work ...
|
|
214
391
|
replicas computer record stop
|
|
215
|
-
replicas media upload /tmp/walkthrough.mp4
|
|
392
|
+
replicas media upload /tmp/walkthrough.mp4 --name "Walkthrough recording"
|
|
216
393
|
\`\`\`
|
|
217
394
|
Then embed the printed \`\` line in your chat reply. See \`MEDIA.md\`.
|
|
218
395
|
|
|
219
396
|
## Failure modes
|
|
220
397
|
|
|
398
|
+
- **"Computer-use daemon is unavailable"**: the package-managed systemd service did not start. Check \`systemctl status replicas-computer.service\` and its journal.
|
|
221
399
|
- **"Desktop services script missing"**: workspace image is older than this skill. Tell the user - nothing you can do from the CLI side.
|
|
222
400
|
- **\`xdotool ... failed: Can't open display\`**: Xvfb didn't come up. \`replicas computer status\` will show which service is dead and auto-repair the desktop bridge.
|
|
223
401
|
- **Browser doesn't appear after \`launch chrome\`**: run \`replicas computer observe /tmp/state.png\`. Chrome cold-start on the virtual display takes ~500ms but bigger pages take longer.
|
|
@@ -228,17 +406,18 @@ Then embed the printed \`\` line in your chat reply. See \`MEDI
|
|
|
228
406
|
|
|
229
407
|
| Component | Where |
|
|
230
408
|
|---|---|
|
|
231
|
-
| \`xvfb\`, \`openbox\`, \`tint2\`, \`
|
|
232
|
-
|
|
|
233
|
-
|
|
|
409
|
+
| \`xvfb\`, \`openbox\`, \`tint2\`, \`pulseaudio\`, \`xautomation\`, \`xdotool\`, \`scrot\`, \`ffmpeg\`, \`google-chrome\` | Baked into the workspace image |
|
|
410
|
+
| \`replicas-computer\`, desktop assets, and service unit | Installed by \`replicas-computer setup\` during the image build |
|
|
411
|
+
| Xvfb / openbox / tint2 / PulseAudio / stream processes | Managed by \`replicas-computer.service\` |
|
|
412
|
+
| Desktop stream URL (port 6080) | Registered by the engine on startup (authenticated) |
|
|
234
413
|
| Dashboard \`Desktop\` tab | Always available once the engine has registered the preview |
|
|
235
414
|
|
|
236
415
|
You can call any of the input tools (\`click\`, \`type\`, \`screenshot\`, etc.) immediately - the daemons are already running.
|
|
237
416
|
`;
|
|
238
417
|
var COMPUTER_ABILITY = {
|
|
239
418
|
label: "Computer use",
|
|
240
|
-
description: "Drive the workspace's Linux desktop and stream a live
|
|
241
|
-
bullet: "- Driving the workspace Linux desktop (open a browser, click, type, scroll, screenshot, record) and exposing a live
|
|
419
|
+
description: "Drive the workspace's Linux desktop and stream a live H.264 and Opus viewer.",
|
|
420
|
+
bullet: "- Driving the workspace Linux desktop (open a browser, click, type, scroll, screenshot, record) and exposing a live audio and video stream in the dashboard",
|
|
242
421
|
section: SECTION,
|
|
243
422
|
referenceFile: { name: "COMPUTER-USE.md", content: REFERENCE }
|
|
244
423
|
};
|
|
@@ -285,7 +464,7 @@ var DOCKER_ABILITY = {
|
|
|
285
464
|
|
|
286
465
|
// ../shared/src/default-skills/replicas-agent/abilities/github.ts
|
|
287
466
|
var SECTION3 = `### GitHub
|
|
288
|
-
Use the pre-authenticated \`gh\` CLI for
|
|
467
|
+
Use the pre-authenticated \`gh\` CLI for repository workflows and the workspace-scoped TypeScript SDK for GitHub API calls.
|
|
289
468
|
|
|
290
469
|
**Reference:** \`references/GITHUB.md\`
|
|
291
470
|
|
|
@@ -296,7 +475,7 @@ Use this when:
|
|
|
296
475
|
- You need to include images in PR descriptions`;
|
|
297
476
|
var REFERENCE3 = `# GitHub Integration
|
|
298
477
|
|
|
299
|
-
This guide covers how to interact with GitHub from within your Replicas workspace.
|
|
478
|
+
This guide covers how to interact with GitHub from within your Replicas workspace. Prefer \`@replicas/sdk\` for API calls so batching and data processing stay in one TypeScript program. The \`gh\` CLI remains useful for ergonomic pull request and git-adjacent workflows.
|
|
300
479
|
|
|
301
480
|
## Prerequisites
|
|
302
481
|
|
|
@@ -381,17 +560,21 @@ gh run rerun RUN_ID --failed
|
|
|
381
560
|
|
|
382
561
|
### GitHub API (Advanced)
|
|
383
562
|
|
|
384
|
-
For operations not covered by \`gh\` subcommands, use the API
|
|
563
|
+
For operations not covered by \`gh\` subcommands, use the scoped SDK. Pass only a relative GitHub API path:
|
|
385
564
|
|
|
386
|
-
\`\`\`
|
|
387
|
-
|
|
388
|
-
gh api repos/owner/repo/pulls/123/comments
|
|
565
|
+
\`\`\`ts
|
|
566
|
+
import { replicas } from '@replicas/sdk';
|
|
389
567
|
|
|
390
|
-
|
|
391
|
-
|
|
568
|
+
const comments = await replicas.github.request('/repos/owner/repo/pulls/123/comments');
|
|
569
|
+
await replicas.github.request('/repos/owner/repo/issues/123/comments', {
|
|
570
|
+
method: 'POST',
|
|
571
|
+
body: { body: 'Comment text' },
|
|
572
|
+
});
|
|
392
573
|
|
|
393
|
-
|
|
394
|
-
|
|
574
|
+
const result = await replicas.github.request('/graphql', {
|
|
575
|
+
method: 'POST',
|
|
576
|
+
body: { query: '{ viewer { login } }' },
|
|
577
|
+
});
|
|
395
578
|
\`\`\`
|
|
396
579
|
|
|
397
580
|
### Working with PR Reviews
|
|
@@ -413,12 +596,15 @@ gh pr review 123 --request-changes --body "Changes needed"
|
|
|
413
596
|
GitHub does NOT have a public API for uploading images to PRs/issues. When you need to include images:
|
|
414
597
|
- Do NOT use placeholder image URLs
|
|
415
598
|
- Do NOT commit screenshots as files to the repository
|
|
416
|
-
-
|
|
599
|
+
- Run \`replicas media upload <image> --name "<display name>" --share\` and put the printed \`Forge embed\` Markdown in the PR body or comment
|
|
600
|
+
- For a self-contained HTML report, run \`replicas media upload <page.html> --name "<display name>" --access organization\` and put the printed \`Forge link\` Markdown in the PR body or comment. Organization members who are already signed in open it directly; others sign in and return to the page
|
|
601
|
+
- For video, embed a shared poster image linked to the recording's **View in Replicas** URL; GitHub does not reliably render externally hosted video inline
|
|
602
|
+
- Treat the public URL as an opt-in bearer capability; revoke it with \`replicas media revoke <media-id>\` only when the user asks or the media should stop rendering \u2014 revoking breaks embeds already posted on the PR
|
|
417
603
|
- If you were triggered from Slack, also upload the image to the Slack thread so the user can see it directly
|
|
418
604
|
`;
|
|
419
605
|
var GITHUB_ABILITY = {
|
|
420
606
|
label: "GitHub",
|
|
421
|
-
description: "
|
|
607
|
+
description: "GitHub workflows through gh and API calls through @replicas/sdk.",
|
|
422
608
|
bullet: "- Interacting with GitHub (creating PRs, managing issues, using the API, etc.)",
|
|
423
609
|
section: SECTION3,
|
|
424
610
|
referenceFile: { name: "GITHUB.md", content: REFERENCE3 }
|
|
@@ -426,453 +612,144 @@ var GITHUB_ABILITY = {
|
|
|
426
612
|
|
|
427
613
|
// ../shared/src/default-skills/replicas-agent/abilities/gitlab.ts
|
|
428
614
|
var SECTION4 = `### GitLab
|
|
429
|
-
|
|
615
|
+
Use plain git for repository traffic and the workspace-scoped TypeScript SDK for GitLab API operations.
|
|
430
616
|
|
|
431
617
|
**Reference:** \`references/GITLAB.md\`
|
|
432
618
|
|
|
433
619
|
Use this when:
|
|
434
|
-
- A
|
|
435
|
-
- You need to open or update a merge request
|
|
620
|
+
- A repository remote is on gitlab.com or a self-managed GitLab instance
|
|
621
|
+
- You need to open or update a merge request
|
|
622
|
+
- You need GitLab issues, pipelines, comments, or other API data`;
|
|
436
623
|
var REFERENCE4 = `# GitLab Integration
|
|
437
624
|
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
## Prerequisites
|
|
441
|
-
|
|
442
|
-
Git credentials for the workspace's GitLab hosts are pre-configured in \`~/.git-credentials\` and refreshed automatically. Plain \`git fetch\` / \`git pull\` / \`git push\` over HTTPS work with no additional setup.
|
|
443
|
-
|
|
444
|
-
- Never ask the user for a GitLab token or PAT \u2014 credentials are already wired. If a push fails with an authentication error, report it to the user instead of working around it.
|
|
445
|
-
- There is no \`glab\` CLI in the workspace, and \`gh\` only works for GitHub remotes. Check a repo's host with \`git remote get-url origin\` before choosing the GitHub or GitLab workflow.
|
|
625
|
+
Git credentials are preconfigured for fetch, pull, and push. Use \`@replicas/sdk\` for API traffic; never inspect \`~/.git-credentials\`, request a token, or send a credential with curl.
|
|
446
626
|
|
|
447
|
-
## Merge
|
|
627
|
+
## Merge requests
|
|
448
628
|
|
|
449
|
-
Create a merge request directly from a push
|
|
629
|
+
Create a merge request directly from a push:
|
|
450
630
|
|
|
451
631
|
\`\`\`bash
|
|
452
|
-
git push -o merge_request.create -o merge_request.target=<default-branch> origin HEAD
|
|
632
|
+
git push -o merge_request.create -o merge_request.target=<default-branch> -o merge_request.title="Title" origin HEAD
|
|
453
633
|
\`\`\`
|
|
454
634
|
|
|
455
|
-
Useful options
|
|
635
|
+
Useful options are \`-o merge_request.draft\` and \`-o merge_request.remove_source_branch\`. GitLab prints the merge request URL.
|
|
456
636
|
|
|
457
|
-
|
|
458
|
-
-o merge_request.title="Title"
|
|
459
|
-
-o merge_request.description="Description"
|
|
460
|
-
-o merge_request.draft # open as draft
|
|
461
|
-
-o merge_request.remove_source_branch # delete branch on merge
|
|
462
|
-
\`\`\`
|
|
637
|
+
## API requests
|
|
463
638
|
|
|
464
|
-
|
|
639
|
+
Pass a relative path below \`/api/v4\`. Replicas selects the credential only from GitLab hosts attached to the workspace repositories.
|
|
465
640
|
|
|
466
|
-
|
|
641
|
+
\`\`\`ts
|
|
642
|
+
import { replicas } from '@replicas/sdk';
|
|
467
643
|
|
|
468
|
-
|
|
644
|
+
const project = encodeURIComponent('group/project');
|
|
645
|
+
const mergeRequests = await replicas.gitlab.request(
|
|
646
|
+
\`/projects/\${project}/merge_requests?state=opened\`,
|
|
647
|
+
);
|
|
469
648
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
649
|
+
await replicas.gitlab.request(\`/projects/\${project}/issues/42/notes\`, {
|
|
650
|
+
method: 'POST',
|
|
651
|
+
body: { body: 'Update from Replicas' },
|
|
652
|
+
});
|
|
473
653
|
\`\`\`
|
|
474
654
|
|
|
475
|
-
|
|
655
|
+
When a workspace contains repositories from more than one GitLab host, pass the exact repository host:
|
|
656
|
+
|
|
657
|
+
\`\`\`ts
|
|
658
|
+
await replicas.gitlab.request('/projects/group%2Frepo/pipelines', {
|
|
659
|
+
host: 'gitlab.example.com',
|
|
660
|
+
});
|
|
661
|
+
\`\`\`
|
|
662
|
+
|
|
663
|
+
## Descriptions and media
|
|
664
|
+
|
|
665
|
+
API updates replace the entire merge request description, so always preserve the required Replicas footer and co-author trailers. For media, first run \`replicas media upload\` as documented in \`MEDIA.md\`; use GitLab's project uploads endpoint only when a native repository-protected embed is required.
|
|
476
666
|
`;
|
|
477
667
|
var GITLAB_ABILITY = {
|
|
478
668
|
label: "GitLab",
|
|
479
|
-
description: "
|
|
669
|
+
description: "Git for repository traffic and @replicas/sdk for the GitLab API.",
|
|
480
670
|
bullet: "- Interacting with GitLab (pushing to GitLab repos, opening merge requests)",
|
|
481
671
|
section: SECTION4,
|
|
482
672
|
referenceFile: { name: "GITLAB.md", content: REFERENCE4 }
|
|
483
673
|
};
|
|
484
674
|
|
|
485
|
-
// ../shared/src/default-skills/replicas-agent/abilities/google.ts
|
|
486
|
-
var SECTION5 = `### Google Workspace (Docs, Sheets, Forms, Drive)
|
|
487
|
-
Create and edit Google Docs, Sheets, and Forms via the Replicas gateway. Files are owned by Replicas \u2014 the integration cannot access pre-existing Google content created outside of it.
|
|
488
|
-
|
|
489
|
-
**Reference:** \`references/GOOGLE.md\`
|
|
490
|
-
|
|
491
|
-
Use this when:
|
|
492
|
-
- You need to create or edit a Google Doc, Sheet, or Form
|
|
493
|
-
- You need to share, rename, move, or delete a Replicas-created Google file
|
|
494
|
-
- You need to read responses from a Replicas-created Google Form`;
|
|
495
|
-
var REFERENCE5 = `# Google Workspace (Docs, Sheets, Forms, Drive)
|
|
496
|
-
|
|
497
|
-
This guide covers how to create and edit Google Docs, Sheets, and Forms \u2014 plus do basic Drive file operations \u2014 from inside a Replicas workspace, using the monolith as a gateway to Google's APIs.
|
|
498
|
-
|
|
499
|
-
## Prerequisites
|
|
500
|
-
|
|
501
|
-
The integration is configured at the org or user level by the Replicas admin. From inside a workspace you don't have a Google access token directly; instead you call the monolith's \`/v1/gdrive/*\` endpoints, authenticated with your workspace's engine secret. The monolith refreshes the org's (or user's) Google access token and proxies the call.
|
|
502
|
-
|
|
503
|
-
Quick check that the integration is connected:
|
|
504
|
-
|
|
505
|
-
\`\`\`bash
|
|
506
|
-
curl -s -X GET "$MONOLITH_URL/v1/gdrive/credentials" \\
|
|
507
|
-
-H "Authorization: Bearer $REPLICAS_ENGINE_SECRET" \\
|
|
508
|
-
-H "X-Workspace-Id: $WORKSPACE_ID"
|
|
509
|
-
\`\`\`
|
|
510
|
-
|
|
511
|
-
- If \`hasCredentials\` is \`true\`: you're good to go.
|
|
512
|
-
- If \`hasCredentials\` is \`false\`: Google has not been connected for this org. Ask the user to go to **Settings \u2192 Integrations \u2192 Google** in the [Replicas dashboard](https://replicas.dev) and connect a Google account. Do not attempt Google operations until it's connected.
|
|
513
|
-
|
|
514
|
-
Standard auth headers used by every call below:
|
|
515
|
-
|
|
516
|
-
\`\`\`
|
|
517
|
-
Authorization: Bearer $REPLICAS_ENGINE_SECRET
|
|
518
|
-
X-Workspace-Id: $WORKSPACE_ID
|
|
519
|
-
\`\`\`
|
|
520
|
-
|
|
521
|
-
For brevity the examples below use a shell variable:
|
|
522
|
-
|
|
523
|
-
\`\`\`bash
|
|
524
|
-
GDRIVE_AUTH=(-H "Authorization: Bearer $REPLICAS_ENGINE_SECRET" -H "X-Workspace-Id: $WORKSPACE_ID")
|
|
525
|
-
\`\`\`
|
|
526
|
-
|
|
527
|
-
## Important constraint: drive.file scope
|
|
528
|
-
|
|
529
|
-
The integration uses the **sensitive-tier \`drive.file\` scope**. That means Replicas can only read and edit Google files **it created itself**. It **cannot**:
|
|
530
|
-
|
|
531
|
-
- Read or edit a user's pre-existing Google Docs, Sheets, or Forms \u2014 even ones that were shared with the connected Google account.
|
|
532
|
-
- List or search the user's broader Drive.
|
|
533
|
-
- Touch any file that was not created via these gateway endpoints.
|
|
534
|
-
|
|
535
|
-
If the user asks you to edit an existing doc that Replicas didn't create, tell them this constraint and offer to create a new doc that mirrors what they want.
|
|
536
|
-
|
|
537
|
-
## Google Docs
|
|
538
|
-
|
|
539
|
-
### Create a new doc
|
|
540
|
-
|
|
541
|
-
\`\`\`bash
|
|
542
|
-
curl -s -X POST "$MONOLITH_URL/v1/gdrive/docs" "\${GDRIVE_AUTH[@]}" \\
|
|
543
|
-
-H "Content-Type: application/json" \\
|
|
544
|
-
-d '{"title":"Meeting notes 2026-05-14"}'
|
|
545
|
-
\`\`\`
|
|
546
|
-
|
|
547
|
-
Returns the full Doc object; grab \`.documentId\` for follow-up calls.
|
|
548
|
-
|
|
549
|
-
### Read a doc
|
|
550
|
-
|
|
551
|
-
\`\`\`bash
|
|
552
|
-
curl -s "$MONOLITH_URL/v1/gdrive/docs/$DOC_ID" "\${GDRIVE_AUTH[@]}"
|
|
553
|
-
\`\`\`
|
|
554
|
-
|
|
555
|
-
Returns the full document structure \u2014 \`body.content\` is an ordered list of structural elements (paragraphs, tables, etc.) with character indexes you can target for edits.
|
|
556
|
-
|
|
557
|
-
### Edit a doc (batchUpdate)
|
|
558
|
-
|
|
559
|
-
The Docs API edits use a list of [structural requests](https://developers.google.com/workspace/docs/api/reference/rest/v1/documents/request). Insert text, then style it; or insert tables, images, page breaks, etc.
|
|
560
|
-
|
|
561
|
-
\`\`\`bash
|
|
562
|
-
curl -s -X POST "$MONOLITH_URL/v1/gdrive/docs/$DOC_ID/batchUpdate" "\${GDRIVE_AUTH[@]}" \\
|
|
563
|
-
-H "Content-Type: application/json" \\
|
|
564
|
-
-d '{
|
|
565
|
-
"requests": [
|
|
566
|
-
{ "insertText": { "location": { "index": 1 }, "text": "Hello, world!\\n" } }
|
|
567
|
-
]
|
|
568
|
-
}'
|
|
569
|
-
\`\`\`
|
|
570
|
-
|
|
571
|
-
Common request types:
|
|
572
|
-
|
|
573
|
-
- \`insertText\` \u2014 insert plain text at a given index
|
|
574
|
-
- \`deleteContentRange\` \u2014 delete a range
|
|
575
|
-
- \`replaceAllText\` \u2014 find-and-replace
|
|
576
|
-
- \`updateTextStyle\` \u2014 bold/italic/colors/fonts/sizes for a range
|
|
577
|
-
- \`updateParagraphStyle\` \u2014 headings (\`HEADING_1\`..\`HEADING_6\`), alignment, spacing
|
|
578
|
-
- \`createParagraphBullets\` \u2014 turn paragraphs into bulleted/numbered lists
|
|
579
|
-
- \`insertTable\` \u2014 insert a table
|
|
580
|
-
- \`insertInlineImage\` \u2014 insert an image from a URL
|
|
581
|
-
|
|
582
|
-
Edits are verbose but powerful. Prefer batching many requests into a single \`batchUpdate\` call rather than making many round trips \u2014 it's faster and keeps the doc state consistent.
|
|
583
|
-
|
|
584
|
-
## Google Sheets
|
|
585
|
-
|
|
586
|
-
### Create a new spreadsheet
|
|
587
|
-
|
|
588
|
-
\`\`\`bash
|
|
589
|
-
curl -s -X POST "$MONOLITH_URL/v1/gdrive/sheets" "\${GDRIVE_AUTH[@]}" \\
|
|
590
|
-
-H "Content-Type: application/json" \\
|
|
591
|
-
-d '{"title":"Q2 metrics"}'
|
|
592
|
-
\`\`\`
|
|
593
|
-
|
|
594
|
-
Returns the full Spreadsheet object; grab \`.spreadsheetId\`.
|
|
595
|
-
|
|
596
|
-
### Read a range of cells
|
|
597
|
-
|
|
598
|
-
\`\`\`bash
|
|
599
|
-
# Range is in A1 notation, e.g. "Sheet1!A1:C10"
|
|
600
|
-
curl -s "$MONOLITH_URL/v1/gdrive/sheets/$SHEET_ID/values/$(printf %s 'Sheet1!A1:C10' | jq -sRr @uri)" \\
|
|
601
|
-
"\${GDRIVE_AUTH[@]}"
|
|
602
|
-
\`\`\`
|
|
603
|
-
|
|
604
|
-
### Write a range of cells
|
|
605
|
-
|
|
606
|
-
\`\`\`bash
|
|
607
|
-
curl -s -X PUT \\
|
|
608
|
-
"$MONOLITH_URL/v1/gdrive/sheets/$SHEET_ID/values/$(printf %s 'Sheet1!A1:B2' | jq -sRr @uri)?valueInputOption=USER_ENTERED" \\
|
|
609
|
-
"\${GDRIVE_AUTH[@]}" \\
|
|
610
|
-
-H "Content-Type: application/json" \\
|
|
611
|
-
-d '{
|
|
612
|
-
"range": "Sheet1!A1:B2",
|
|
613
|
-
"majorDimension": "ROWS",
|
|
614
|
-
"values": [
|
|
615
|
-
["Name", "Revenue"],
|
|
616
|
-
["Q1", "=SUM(B3:B100)"]
|
|
617
|
-
]
|
|
618
|
-
}'
|
|
619
|
-
\`\`\`
|
|
620
|
-
|
|
621
|
-
\`valueInputOption=USER_ENTERED\` makes formulas evaluate as if a human typed them. Use \`RAW\` to write literal strings.
|
|
622
|
-
|
|
623
|
-
### Bulk operations (formatting, charts, new tabs, etc.)
|
|
624
|
-
|
|
625
|
-
\`\`\`bash
|
|
626
|
-
curl -s -X POST "$MONOLITH_URL/v1/gdrive/sheets/$SHEET_ID/batchUpdate" "\${GDRIVE_AUTH[@]}" \\
|
|
627
|
-
-H "Content-Type: application/json" \\
|
|
628
|
-
-d '{ "requests": [ { "addSheet": { "properties": { "title": "Raw data" } } } ] }'
|
|
629
|
-
\`\`\`
|
|
630
|
-
|
|
631
|
-
\`batchUpdate\` accepts an array of [Sheets API requests](https://developers.google.com/workspace/sheets/api/reference/rest/v4/spreadsheets/request) \u2014 addSheet, updateSheetProperties, repeatCell, addChart, autoResizeDimensions, etc.
|
|
632
|
-
|
|
633
|
-
## Google Forms
|
|
634
|
-
|
|
635
|
-
### Create a new form
|
|
636
|
-
|
|
637
|
-
\`\`\`bash
|
|
638
|
-
curl -s -X POST "$MONOLITH_URL/v1/gdrive/forms" "\${GDRIVE_AUTH[@]}" \\
|
|
639
|
-
-H "Content-Type: application/json" \\
|
|
640
|
-
-d '{"title":"Customer feedback"}'
|
|
641
|
-
\`\`\`
|
|
642
|
-
|
|
643
|
-
Returns the form. Grab \`.formId\`.
|
|
644
|
-
|
|
645
|
-
### Add questions / edit form structure
|
|
646
|
-
|
|
647
|
-
The Forms API uses its own \`batchUpdate\`:
|
|
648
|
-
|
|
649
|
-
\`\`\`bash
|
|
650
|
-
curl -s -X POST "$MONOLITH_URL/v1/gdrive/forms/$FORM_ID/batchUpdate" "\${GDRIVE_AUTH[@]}" \\
|
|
651
|
-
-H "Content-Type: application/json" \\
|
|
652
|
-
-d '{
|
|
653
|
-
"requests": [
|
|
654
|
-
{
|
|
655
|
-
"createItem": {
|
|
656
|
-
"item": {
|
|
657
|
-
"title": "How likely are you to recommend us?",
|
|
658
|
-
"questionItem": {
|
|
659
|
-
"question": {
|
|
660
|
-
"required": true,
|
|
661
|
-
"scaleQuestion": { "low": 0, "high": 10, "lowLabel": "Not at all", "highLabel": "Very likely" }
|
|
662
|
-
}
|
|
663
|
-
}
|
|
664
|
-
},
|
|
665
|
-
"location": { "index": 0 }
|
|
666
|
-
}
|
|
667
|
-
}
|
|
668
|
-
]
|
|
669
|
-
}'
|
|
670
|
-
\`\`\`
|
|
671
|
-
|
|
672
|
-
See the [Forms API Request reference](https://developers.google.com/workspace/forms/api/reference/rest/v1/forms/request) for all supported request types \u2014 text questions, multiple choice, checkboxes, scale, grid, date, time, file upload, section breaks, branching logic, quiz settings, etc.
|
|
673
|
-
|
|
674
|
-
### Read responses
|
|
675
|
-
|
|
676
|
-
\`\`\`bash
|
|
677
|
-
curl -s "$MONOLITH_URL/v1/gdrive/forms/$FORM_ID/responses" "\${GDRIVE_AUTH[@]}"
|
|
678
|
-
\`\`\`
|
|
679
|
-
|
|
680
|
-
Pagination: pass \`?pageToken=...&pageSize=...\` to walk through responses.
|
|
681
|
-
|
|
682
|
-
## Drive operations (only on Replicas-created files)
|
|
683
|
-
|
|
684
|
-
### Share a file with a person
|
|
685
|
-
|
|
686
|
-
\`\`\`bash
|
|
687
|
-
curl -s -X POST "$MONOLITH_URL/v1/gdrive/files/$FILE_ID/permissions" "\${GDRIVE_AUTH[@]}" \\
|
|
688
|
-
-H "Content-Type: application/json" \\
|
|
689
|
-
-d '{
|
|
690
|
-
"type": "user",
|
|
691
|
-
"role": "writer",
|
|
692
|
-
"emailAddress": "alice@example.com",
|
|
693
|
-
"sendNotificationEmail": true
|
|
694
|
-
}'
|
|
695
|
-
\`\`\`
|
|
696
|
-
|
|
697
|
-
Roles: \`reader\`, \`commenter\`, \`writer\`. Types: \`user\`, \`group\`, \`domain\`, \`anyone\`.
|
|
698
|
-
|
|
699
|
-
### Make a file viewable by anyone with the link
|
|
700
|
-
|
|
701
|
-
\`\`\`bash
|
|
702
|
-
curl -s -X POST "$MONOLITH_URL/v1/gdrive/files/$FILE_ID/permissions" "\${GDRIVE_AUTH[@]}" \\
|
|
703
|
-
-H "Content-Type: application/json" \\
|
|
704
|
-
-d '{ "type": "anyone", "role": "reader" }'
|
|
705
|
-
\`\`\`
|
|
706
|
-
|
|
707
|
-
### List existing permissions
|
|
708
|
-
|
|
709
|
-
\`\`\`bash
|
|
710
|
-
curl -s "$MONOLITH_URL/v1/gdrive/files/$FILE_ID/permissions" "\${GDRIVE_AUTH[@]}"
|
|
711
|
-
\`\`\`
|
|
712
|
-
|
|
713
|
-
### Rename / move a file
|
|
714
|
-
|
|
715
|
-
\`\`\`bash
|
|
716
|
-
curl -s -X PATCH "$MONOLITH_URL/v1/gdrive/files/$FILE_ID" "\${GDRIVE_AUTH[@]}" \\
|
|
717
|
-
-H "Content-Type: application/json" \\
|
|
718
|
-
-d '{"name":"Final report.docx"}'
|
|
719
|
-
\`\`\`
|
|
720
|
-
|
|
721
|
-
To move into a folder, also pass \`addParents\` and \`removeParents\` as query parameters per [Drive API docs](https://developers.google.com/workspace/drive/api/reference/rest/v3/files/update).
|
|
722
|
-
|
|
723
|
-
### Get file metadata
|
|
724
|
-
|
|
725
|
-
\`\`\`bash
|
|
726
|
-
curl -s "$MONOLITH_URL/v1/gdrive/files/$FILE_ID?fields=id,name,mimeType,webViewLink,parents,modifiedTime" \\
|
|
727
|
-
"\${GDRIVE_AUTH[@]}"
|
|
728
|
-
\`\`\`
|
|
729
|
-
|
|
730
|
-
\`webViewLink\` is the human-shareable URL that opens in Google Docs/Sheets/Forms.
|
|
731
|
-
|
|
732
|
-
### Delete a file
|
|
733
|
-
|
|
734
|
-
\`\`\`bash
|
|
735
|
-
curl -s -X DELETE "$MONOLITH_URL/v1/gdrive/files/$FILE_ID" "\${GDRIVE_AUTH[@]}"
|
|
736
|
-
\`\`\`
|
|
737
|
-
|
|
738
|
-
### List Replicas-created files
|
|
739
|
-
|
|
740
|
-
\`\`\`bash
|
|
741
|
-
curl -s "$MONOLITH_URL/v1/gdrive/files?pageSize=50" "\${GDRIVE_AUTH[@]}"
|
|
742
|
-
\`\`\`
|
|
743
|
-
|
|
744
|
-
Pass \`?q=...\` to filter using [Drive API search syntax](https://developers.google.com/workspace/drive/api/guides/search-files). Only files the app created or was granted access to will appear.
|
|
745
|
-
|
|
746
|
-
## Tips
|
|
747
|
-
|
|
748
|
-
- **Always return the \`webViewLink\`** to the user when you create or edit a file so they can open it. Get it from \`/v1/gdrive/files/$FILE_ID?fields=webViewLink\` or from \`documents.documentId\` \u2192 \`https://docs.google.com/document/d/$ID/edit\`.
|
|
749
|
-
- **Batch your edits.** A single \`batchUpdate\` with 20 requests beats 20 round-trips.
|
|
750
|
-
- **Watch error responses.** A 412 from the gateway means the org has no Google credentials connected \u2014 tell the user to connect Google in the dashboard. Other 4xx responses come straight from Google and usually explain the issue clearly.
|
|
751
|
-
`;
|
|
752
|
-
var GOOGLE_ABILITY = {
|
|
753
|
-
label: "Google Workspace",
|
|
754
|
-
description: "Create / edit Docs, Sheets, Forms, and Drive files via the Replicas gateway.",
|
|
755
|
-
bullet: "- Interacting with Google Workspace (creating and editing Docs, Sheets, and Forms, sharing files, reading form responses, etc.)",
|
|
756
|
-
section: SECTION5,
|
|
757
|
-
referenceFile: { name: "GOOGLE.md", content: REFERENCE5 }
|
|
758
|
-
};
|
|
759
|
-
|
|
760
675
|
// ../shared/src/default-skills/replicas-agent/abilities/linear.ts
|
|
761
|
-
var
|
|
762
|
-
Fetch issues, update state, add comments, and search
|
|
676
|
+
var SECTION5 = `### Linear
|
|
677
|
+
Fetch issues, update state, add comments, and search through the workspace-scoped TypeScript SDK.
|
|
763
678
|
|
|
764
679
|
**Reference:** \`references/LINEAR.md\`
|
|
765
680
|
|
|
766
681
|
Use this when:
|
|
767
682
|
- You encounter a Linear issue link and need to understand the task
|
|
768
|
-
- You need to update an issue's state
|
|
769
|
-
- You need to
|
|
770
|
-
var
|
|
771
|
-
|
|
772
|
-
This guide covers how to interact with Linear from within your Replicas workspace.
|
|
773
|
-
|
|
774
|
-
## Prerequisites
|
|
775
|
-
|
|
776
|
-
Check that the workspace has the Replicas engine credentials needed to proxy Linear requests:
|
|
777
|
-
|
|
778
|
-
\`\`\`bash
|
|
779
|
-
test -n "$MONOLITH_URL" && test -n "$REPLICAS_ENGINE_SECRET" && test -n "$WORKSPACE_ID" && echo set
|
|
780
|
-
\`\`\`
|
|
781
|
-
|
|
782
|
-
- If **set**: Use the Replicas Linear proxy below. It refreshes Linear OAuth tokens for you.
|
|
783
|
-
- If **not set**: The workspace cannot authenticate to Replicas. Let the user know and do not attempt Linear operations.
|
|
784
|
-
|
|
785
|
-
Then verify Linear is connected:
|
|
786
|
-
|
|
787
|
-
\`\`\`bash
|
|
788
|
-
curl -fsS -X POST "$MONOLITH_URL/v1/engine/linear/refresh-token" \\
|
|
789
|
-
-H "Authorization: Bearer $REPLICAS_ENGINE_SECRET" \\
|
|
790
|
-
-H "X-Workspace-Id: $WORKSPACE_ID" \\
|
|
791
|
-
-H "Content-Type: application/json" \\
|
|
792
|
-
-d '{}' >/dev/null
|
|
793
|
-
\`\`\`
|
|
794
|
-
|
|
795
|
-
If this fails because no Linear credential is connected, the user needs to connect Linear in the [Replicas dashboard](https://replicas.dev) under their organization's integration settings.
|
|
683
|
+
- You need to update an issue's state or add a comment
|
|
684
|
+
- You need to search Linear issues`;
|
|
685
|
+
var REFERENCE5 = `# Linear Integration
|
|
796
686
|
|
|
797
|
-
|
|
687
|
+
Use \`@replicas/sdk\`; never request or handle a Linear OAuth token. Replicas refreshes the organization credential behind the gateway.
|
|
798
688
|
|
|
799
|
-
|
|
689
|
+
## Check access
|
|
800
690
|
|
|
801
|
-
\`\`\`
|
|
802
|
-
|
|
803
|
-
curl -sS -X POST "$MONOLITH_URL/v1/engine/linear/graphql" \\
|
|
804
|
-
-H "Authorization: Bearer $REPLICAS_ENGINE_SECRET" \\
|
|
805
|
-
-H "X-Workspace-Id: $WORKSPACE_ID" \\
|
|
806
|
-
-H "Content-Type: application/json" \\
|
|
807
|
-
--data-binary @-
|
|
808
|
-
}
|
|
809
|
-
\`\`\`
|
|
810
|
-
|
|
811
|
-
### Fetching an Issue
|
|
812
|
-
|
|
813
|
-
If you encounter a Linear issue link (e.g. \`https://linear.app/team/issue/ENG-123\`), the identifier is the last path segment (\`ENG-123\`).
|
|
814
|
-
|
|
815
|
-
\`\`\`bash
|
|
816
|
-
jq -n --arg identifier "ENG-123" '{
|
|
817
|
-
query: "query($identifier: String!) { issue(id: $identifier) { id identifier title description state { name } assignee { name } parent { identifier title description } } }",
|
|
818
|
-
variables: { identifier: $identifier }
|
|
819
|
-
}' | linear_graphql
|
|
820
|
-
\`\`\`
|
|
821
|
-
|
|
822
|
-
### Updating Issue State
|
|
691
|
+
\`\`\`ts
|
|
692
|
+
import { replicas } from '@replicas/sdk';
|
|
823
693
|
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
query: "mutation($issueId: String!, $stateId: String!) { issueUpdate(id: $issueId, input: { stateId: $stateId }) { success issue { identifier state { name } } } }",
|
|
827
|
-
variables: { issueId: $issueId, stateId: $stateId }
|
|
828
|
-
}' | linear_graphql
|
|
694
|
+
const { native } = await replicas.integrations.list();
|
|
695
|
+
if (!native.linear) throw new Error('Connect Linear in Replicas organization integrations first.');
|
|
829
696
|
\`\`\`
|
|
830
697
|
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
### Adding a Comment
|
|
698
|
+
## Fetch an issue
|
|
834
699
|
|
|
835
|
-
\`\`\`
|
|
836
|
-
|
|
837
|
-
query:
|
|
838
|
-
|
|
839
|
-
}
|
|
700
|
+
\`\`\`ts
|
|
701
|
+
const issue = await replicas.linear.graphql({
|
|
702
|
+
query: \`query($identifier: String!) {
|
|
703
|
+
issue(id: $identifier) {
|
|
704
|
+
id identifier title description state { id name } assignee { name }
|
|
705
|
+
parent { identifier title description }
|
|
706
|
+
}
|
|
707
|
+
}\`,
|
|
708
|
+
variables: { identifier: 'ENG-123' },
|
|
709
|
+
});
|
|
840
710
|
\`\`\`
|
|
841
711
|
|
|
842
|
-
|
|
712
|
+
## Update and comment in one script
|
|
843
713
|
|
|
844
|
-
\`\`\`
|
|
845
|
-
|
|
846
|
-
query:
|
|
847
|
-
|
|
848
|
-
}
|
|
714
|
+
\`\`\`ts
|
|
715
|
+
await replicas.linear.graphql({
|
|
716
|
+
query: \`mutation($issueId: String!, $stateId: String!) {
|
|
717
|
+
issueUpdate(id: $issueId, input: { stateId: $stateId }) { success }
|
|
718
|
+
}\`,
|
|
719
|
+
variables: { issueId, stateId },
|
|
720
|
+
});
|
|
721
|
+
await replicas.linear.graphql({
|
|
722
|
+
query: \`mutation($issueId: String!, $body: String!) {
|
|
723
|
+
commentCreate(input: { issueId: $issueId, body: $body }) { success comment { id } }
|
|
724
|
+
}\`,
|
|
725
|
+
variables: { issueId, body: 'Implemented in PR #123.' },
|
|
726
|
+
});
|
|
849
727
|
\`\`\`
|
|
850
728
|
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
The Linear GraphQL API supports creating issues, managing projects, labels, cycles, and more. For the full schema and documentation, see: https://developers.linear.app/docs/graphql/working-with-the-graphql-api
|
|
729
|
+
Search with \`containsIgnoreCase: $query\` filters, for example on both \`title\` and \`description\`. Batch independent queries with \`Promise.all\` to save round trips.
|
|
854
730
|
`;
|
|
855
731
|
var LINEAR_ABILITY = {
|
|
856
732
|
label: "Linear",
|
|
857
|
-
description: "Fetch issues,
|
|
733
|
+
description: "Fetch issues, comment, and update state through @replicas/sdk.",
|
|
858
734
|
bullet: "- Interacting with Linear (fetching issues, updating state, commenting, etc.)",
|
|
859
|
-
section:
|
|
860
|
-
referenceFile: { name: "LINEAR.md", content:
|
|
735
|
+
section: SECTION5,
|
|
736
|
+
referenceFile: { name: "LINEAR.md", content: REFERENCE5 }
|
|
861
737
|
};
|
|
862
738
|
|
|
863
739
|
// ../shared/src/default-skills/replicas-agent/abilities/media.ts
|
|
864
|
-
var
|
|
865
|
-
Share screenshots, screen recordings, generated diagrams,
|
|
740
|
+
var SECTION6 = `### Media
|
|
741
|
+
Share screenshots, screen recordings, generated diagrams, audio clips, and self-contained HTML pages through Replicas and external messages.
|
|
866
742
|
|
|
867
743
|
**Reference:** \`references/MEDIA.md\`
|
|
868
744
|
|
|
869
745
|
Use this when:
|
|
870
746
|
- You produce a screenshot, recording, generated image, or audio clip the user should see
|
|
747
|
+
- You produce a self-contained HTML report that should be linked from a pull request
|
|
871
748
|
- You record video output (browser automation, screen capture) \u2014 including the recommended aspect ratio and FPS
|
|
872
749
|
- You need to embed media in a Slack/Linear/GitHub message AND keep a referenceable copy in the Replicas dashboard`;
|
|
873
|
-
var
|
|
750
|
+
var REFERENCE6 = `# Media (Screenshots, Recordings, Audio, HTML)
|
|
874
751
|
|
|
875
|
-
This guide covers how to share screenshots, screen recordings, generated diagrams,
|
|
752
|
+
This guide covers how to share screenshots, screen recordings, generated diagrams, audio clips, and self-contained HTML pages through Replicas and external surfaces (Slack, Linear, GitHub).
|
|
876
753
|
|
|
877
754
|
## Prerequisites
|
|
878
755
|
|
|
@@ -883,7 +760,7 @@ The \`replicas\` CLI is pre-installed and authenticated in your workspace. No ad
|
|
|
883
760
|
Upload to Replicas in these cases \u2014 and **only** these cases:
|
|
884
761
|
|
|
885
762
|
1. **Media you produce.** Any screenshot, screen recording, generated diagram, or audio clip you create that the user might want to see. Upload before doing anything else with the file (analyzing, deleting, sending elsewhere). This applies even when you're also sending the file to Slack, Linear, GitHub, etc.
|
|
886
|
-
2. **Files the user explicitly asks you to upload.** If the user sends or points at a file (image, video, audio) and asks you to upload it, run \`replicas media upload\`. Otherwise leave it alone \u2014 files in the workspace the user did not ask about should not be auto-uploaded as media.
|
|
763
|
+
2. **Files the user explicitly asks you to upload.** If the user sends or points at a file (image, video, audio, HTML) and asks you to upload it, run \`replicas media upload\`. Otherwise leave it alone \u2014 files in the workspace the user did not ask about should not be auto-uploaded as media.
|
|
887
764
|
3. **Anything you plan to share externally (Slack, Linear, GitHub, etc.).** Upload to Replicas *in addition to* the platform's native upload. Never as a replacement.
|
|
888
765
|
|
|
889
766
|
If none of these apply, don't upload.
|
|
@@ -891,24 +768,60 @@ If none of these apply, don't upload.
|
|
|
891
768
|
## Uploading to Replicas
|
|
892
769
|
|
|
893
770
|
\`\`\`bash
|
|
894
|
-
replicas media upload <path-to-file> [<path-to-file> ...]
|
|
771
|
+
replicas media upload <path-to-file> --name "<display name>" [<path-to-file> --name "<display name>" ...]
|
|
772
|
+
\`\`\`
|
|
773
|
+
|
|
774
|
+
Pass one or more file paths, each with a required \`--name\` (see below). Uploading several files in a single invocation is preferred over running the command repeatedly: it's faster and keeps the output grouped.
|
|
775
|
+
|
|
776
|
+
### Name each upload
|
|
777
|
+
|
|
778
|
+
\`--name\` is **required** and sets the short, human-readable display name shown in the Replicas dashboard (the backend still tracks the asset by UUID). Make it descriptive of the content, e.g. \`login-page\`, \`checkout-error\`, or \`db-latency-chart\`. Never use a generic value like \`screenshot\` or \`image\`.
|
|
779
|
+
|
|
780
|
+
\`\`\`bash
|
|
781
|
+
replicas media upload /tmp/shot.png --name "Login page after fix"
|
|
782
|
+
\`\`\`
|
|
783
|
+
|
|
784
|
+
Pass one \`--name\` per file, in the same order as the file paths:
|
|
785
|
+
|
|
786
|
+
\`\`\`bash
|
|
787
|
+
replicas media upload /tmp/before.png /tmp/after.png --name "Before fix" --name "After fix"
|
|
895
788
|
\`\`\`
|
|
896
789
|
|
|
897
|
-
|
|
790
|
+
Names are normalized to hyphen-case (spaces become hyphens, e.g. \`Login page\` is stored as \`login-page\`). Give every asset a **distinct** name \u2014 duplicates are not rejected, but Replicas keeps the first as-is and adds a \`-1\`, \`-2\` suffix to later collisions, which produces confusing lists, so pick unique names yourself.
|
|
898
791
|
|
|
899
|
-
For each file, the CLI prints two lines:
|
|
792
|
+
For each media file, the CLI prints two lines:
|
|
900
793
|
|
|
901
794
|
1. \`\` \u2014 the chat embed (renders inline in Replicas chat **only** \u2014 see below).
|
|
902
|
-
2. \`View in Replicas: <deep-link>\` \u2014 a per-file dashboard URL of the form \`https://tryreplicas.com/workspaces/<workspace-id>?
|
|
795
|
+
2. \`View in Replicas: <deep-link>\` \u2014 a per-file dashboard URL of the form \`https://tryreplicas.com/workspaces/<workspace-id>?media=<media-id>\` that opens directly to that specific file.
|
|
903
796
|
|
|
904
797
|
Match each "View in Replicas" line to the embed line directly above it \u2014 that's the deep link for that file.
|
|
905
798
|
|
|
906
|
-
|
|
799
|
+
HTML uploads print an \`HTML page\` Markdown link instead of an image embed.
|
|
800
|
+
|
|
801
|
+
For an HTML page that should stay inside the Replicas organization, create an authenticated link:
|
|
802
|
+
|
|
803
|
+
\`\`\`bash
|
|
804
|
+
replicas media upload recap.html --name "Visual recap" --access organization
|
|
805
|
+
\`\`\`
|
|
806
|
+
|
|
807
|
+
The printed \`Forge link (organization only)\` opens a dedicated page. Signed-in organization members go straight to it; signed-out members sign in and return to the page.
|
|
808
|
+
|
|
809
|
+
Use public access only when the media must render without Replicas authentication:
|
|
810
|
+
|
|
811
|
+
\`\`\`bash
|
|
812
|
+
replicas media upload screenshot.png --name "Login page" --access public
|
|
813
|
+
\`\`\`
|
|
814
|
+
|
|
815
|
+
\`--share\` remains an alias for \`--access public\`. Public access prints an additional \`Forge embed\` for image, video, or audio media, backed by an opt-in bearer URL. Anyone with that unguessable URL can view the asset until it is rotated or revoked. HTML pages only support organization access. Use \`replicas media share <media-id>\` to rotate a public media URL and \`replicas media revoke <media-id>\` to revoke it.
|
|
816
|
+
|
|
817
|
+
## The URLs the CLI gives you \u2014 and where each one is allowed
|
|
907
818
|
|
|
908
819
|
| URL | What it is | Where it's allowed |
|
|
909
820
|
|---|---|---|
|
|
910
821
|
| \`https://api.tryreplicas.com/v1/media/<id>\` (inside \`\`) | Authenticated API redirect to a presigned blob \u2014 requires the Replicas chat session | **Only** in the Replicas chat reply. Nowhere else. |
|
|
911
|
-
| \`https://tryreplicas.com/
|
|
822
|
+
| \`https://tryreplicas.com/organizations/<organization-id>/media/<media-id>\` | Organization-only page printed by \`--access organization\` | Pull requests and other places where every viewer belongs to the Replicas organization. |
|
|
823
|
+
| \`https://api.tryreplicas.com/v1/media/public/<id>/<token>\` | Opt-in public bearer URL printed by \`--access public\` or \`--share\` | GitHub embeds for image, video, or audio media. |
|
|
824
|
+
| \`https://tryreplicas.com/workspaces/<workspace-id>?media=<media-id>\` | Authenticated dashboard deep link that opens the media file in the workspace | Everywhere external \u2014 Slack, GitHub, Linear, PR bodies, comments, emails. |
|
|
912
825
|
|
|
913
826
|
### NEVER link the API redirect URL outside the Replicas chat reply
|
|
914
827
|
|
|
@@ -921,38 +834,36 @@ This rule has no exceptions:
|
|
|
921
834
|
- \u274C Never link to it from any external doc, email, or external chat.
|
|
922
835
|
- \u274C Never include it as a "backup link" in case the dashboard link breaks \u2014 it will always break externally.
|
|
923
836
|
|
|
924
|
-
### ALWAYS link
|
|
837
|
+
### ALWAYS link a Replicas web URL \u2014 never the authenticated API URL \u2014 when referencing media externally
|
|
925
838
|
|
|
926
|
-
|
|
839
|
+
For an organization-only HTML page, use the \`Forge link\` printed by \`--access organization\`. For other authenticated media references, use the dashboard form:
|
|
927
840
|
|
|
928
841
|
\`\`\`
|
|
929
|
-
https://tryreplicas.com/workspaces/<workspace-id>?
|
|
842
|
+
https://tryreplicas.com/workspaces/<workspace-id>?media=<media-id>
|
|
930
843
|
\`\`\`
|
|
931
844
|
|
|
932
|
-
|
|
933
|
-
- \`mode=media\` opens the media tab in the dashboard.
|
|
934
|
-
- \`media=<media-id>\` scrolls to and opens that specific file.
|
|
845
|
+
The \`media=<media-id>\` query param opens that specific file in the workspace.
|
|
935
846
|
|
|
936
|
-
|
|
847
|
+
When you have a specific file in mind, never link the bare workspace URL without the \`media=<id>\` param.
|
|
937
848
|
|
|
938
849
|
Always render as a markdown / mrkdwn hyperlink labeled **View in Replicas** \u2014 never paste a raw URL.
|
|
939
850
|
|
|
940
851
|
## In your Replicas chat reply
|
|
941
852
|
|
|
942
|
-
Include each markdown embed line **verbatim** where you want that file to render inline. The chat substitutes each one with an embedded image, video, or audio player. Multiple embeds can appear in a single reply.
|
|
853
|
+
Include each markdown embed line **verbatim** where you want that file to render inline. The chat substitutes each one with an embedded image, video, or audio player. For HTML, use the \`HTML page\` or \`View in Replicas\` link instead. Multiple embeds can appear in a single reply.
|
|
943
854
|
|
|
944
|
-
After (or alongside) the embeds, include a \`[View in Replicas](<deep-link>)\` hyperlink for each file using the per-file dashboard URL from the CLI. This lets the user
|
|
855
|
+
After (or alongside) the embeds, include a \`[View in Replicas](<deep-link>)\` hyperlink for each file using the per-file dashboard URL from the CLI. This lets the user open the specific item in the workspace.
|
|
945
856
|
|
|
946
857
|
## On external platforms \u2014 upload natively AND link the dashboard
|
|
947
858
|
|
|
948
859
|
Sharing media externally is a **two-step requirement** for every file:
|
|
949
860
|
|
|
950
861
|
1. **Upload the raw bytes natively to that platform** so the recipient sees the media embedded inline in the message itself.
|
|
951
|
-
2. **Link the Replicas dashboard deep link** (\`?
|
|
862
|
+
2. **Link the Replicas dashboard deep link** (\`?media=<id>\` form) so the user can jump straight to the file in the workspace.
|
|
952
863
|
|
|
953
864
|
A link alone is not enough. A native upload alone is not enough. Do both.
|
|
954
865
|
|
|
955
|
-
|
|
866
|
+
GitHub has no native upload API, so use an explicit Replicas public share for screenshots and recording previews. GitLab has a native upload API and should receive the original bytes.
|
|
956
867
|
|
|
957
868
|
### Slack \u2014 embed natively via the 3-step external file upload flow
|
|
958
869
|
|
|
@@ -967,7 +878,7 @@ LENGTH=$(stat -c%s "$FILE" 2>/dev/null || stat -f%z "$FILE")
|
|
|
967
878
|
|
|
968
879
|
# 1. Reserve an upload URL + file ID.
|
|
969
880
|
RESERVE=$(curl -s -G "https://slack.com/api/files.getUploadURLExternal" \\
|
|
970
|
-
-H "Authorization: Bearer
|
|
881
|
+
-H "Authorization: Bearer \${REPLICAS_SLACK_BOT_TOKEN:-$SLACK_BOT_TOKEN}" \\
|
|
971
882
|
--data-urlencode "filename=$FILENAME" \\
|
|
972
883
|
--data-urlencode "length=$LENGTH")
|
|
973
884
|
UPLOAD_URL=$(echo "$RESERVE" | jq -r '.upload_url')
|
|
@@ -982,13 +893,13 @@ curl -s -X POST "$UPLOAD_URL" \\
|
|
|
982
893
|
|
|
983
894
|
# 3. Complete the upload, sharing it into the channel/thread with the dashboard link.
|
|
984
895
|
curl -s -X POST "https://slack.com/api/files.completeUploadExternal" \\
|
|
985
|
-
-H "Authorization: Bearer
|
|
896
|
+
-H "Authorization: Bearer \${REPLICAS_SLACK_BOT_TOKEN:-$SLACK_BOT_TOKEN}" \\
|
|
986
897
|
-H "Content-Type: application/json; charset=utf-8" \\
|
|
987
898
|
-d "{
|
|
988
899
|
\\"files\\": [{\\"id\\": \\"$FILE_ID\\", \\"title\\": \\"Screenshot\\"}],
|
|
989
900
|
\\"channel_id\\": \\"CHANNEL_ID\\",
|
|
990
901
|
\\"thread_ts\\": \\"THREAD_TS\\",
|
|
991
|
-
\\"initial_comment\\": \\"<https://tryreplicas.com/workspaces/<workspace-id>?
|
|
902
|
+
\\"initial_comment\\": \\"<https://tryreplicas.com/workspaces/<workspace-id>?media=<media-id>|View in Replicas>\\"
|
|
992
903
|
}"
|
|
993
904
|
\`\`\`
|
|
994
905
|
|
|
@@ -1016,9 +927,9 @@ ISSUE_UUID=... # the Linear issue's UUID
|
|
|
1016
927
|
MEDIA_ID=... # the media ID printed by \`replicas media upload\`
|
|
1017
928
|
|
|
1018
929
|
linear_graphql() {
|
|
1019
|
-
curl -sS -X POST "$
|
|
930
|
+
curl -sS -X POST "$REPLICAS_MONOLITH_URL/v1/engine/linear/graphql" \\
|
|
1020
931
|
-H "Authorization: Bearer $REPLICAS_ENGINE_SECRET" \\
|
|
1021
|
-
-H "X-Workspace-Id: $
|
|
932
|
+
-H "X-Workspace-Id: $REPLICAS_WORKSPACE_ID" \\
|
|
1022
933
|
-H "Content-Type: application/json" \\
|
|
1023
934
|
--data-binary @-
|
|
1024
935
|
}
|
|
@@ -1048,8 +959,8 @@ curl -s -X PUT "$UPLOAD_URL" \\
|
|
|
1048
959
|
--data-binary @"$FILE"
|
|
1049
960
|
|
|
1050
961
|
# 3. Reference assetUrl inline in the comment body, alongside the dashboard link.
|
|
1051
|
-
BODY=$(printf '\\n\\n[View in Replicas](https://tryreplicas.com/workspaces/%s?
|
|
1052
|
-
"$ASSET_URL" "$
|
|
962
|
+
BODY=$(printf '\\n\\n[View in Replicas](https://tryreplicas.com/workspaces/%s?media=%s)' \\
|
|
963
|
+
"$ASSET_URL" "$REPLICAS_WORKSPACE_ID" "$MEDIA_ID")
|
|
1053
964
|
|
|
1054
965
|
jq -n \\
|
|
1055
966
|
--arg issueId "$ISSUE_UUID" \\
|
|
@@ -1064,20 +975,36 @@ The same flow works for issue bodies (use \`issueCreate\` / \`issueUpdate\` with
|
|
|
1064
975
|
|
|
1065
976
|
Never paste the \`api.tryreplicas.com/v1/media/<id>\` URL into a Linear body or comment \u2014 it will render as a broken image to anyone outside your Replicas chat.
|
|
1066
977
|
|
|
1067
|
-
### GitHub
|
|
978
|
+
### GitHub: inline screenshots and recording previews
|
|
1068
979
|
|
|
1069
|
-
GitHub has **no public API** for uploading
|
|
980
|
+
GitHub has **no public API** for uploading attachments to PRs or issues. Its web UI uses an undocumented internal endpoint that API clients must not emulate. For screenshots, explicitly create a forge share and put the printed \`Forge embed\` Markdown directly in the PR body or comment, followed by the dashboard link:
|
|
981
|
+
|
|
982
|
+
\`\`\`bash
|
|
983
|
+
replicas media upload /tmp/screenshot.png --name "PR screenshot" --share
|
|
984
|
+
\`\`\`
|
|
985
|
+
|
|
986
|
+
GitHub does not reliably render arbitrary external videos as inline players. For a recording:
|
|
987
|
+
|
|
988
|
+
1. Upload the MP4 normally so it remains available in Replicas.
|
|
989
|
+
2. Generate a poster image from the recording: \`ffmpeg -i /tmp/demo.mp4 -frames:v 1 /tmp/demo-poster.png\`.
|
|
990
|
+
3. Upload the poster with \`--share\`.
|
|
991
|
+
4. Wrap the poster embed in a link to the recording's **View in Replicas** URL.
|
|
1070
992
|
|
|
1071
993
|
\`\`\`markdown
|
|
1072
|
-
[
|
|
994
|
+
[](<recording-dashboard-url>)
|
|
1073
995
|
\`\`\`
|
|
1074
996
|
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
-
|
|
1078
|
-
|
|
997
|
+
Public forge shares are bearer capabilities. For private repositories, create one only when the user asked for inline PR media. Revoke with \`replicas media revoke <media-id>\` **only** when the user asks or the media should intentionally stop rendering \u2014 never as routine post-task cleanup, since revoking breaks every embed already posted on the PR.
|
|
998
|
+
|
|
999
|
+
For a self-contained HTML report, upload it with \`--access organization\` and put the printed \`Forge link\` Markdown in the PR body or comment. GitHub cannot render the page inline, so use the normal link rather than image syntax. HTML pages cannot be shared publicly.
|
|
1000
|
+
|
|
1001
|
+
Do **not** commit screenshots to the repo, use placeholder URLs, expose the authenticated \`/v1/media/<id>\` URL, or automate GitHub's undocumented browser upload flow.
|
|
1002
|
+
|
|
1003
|
+
### GitLab: native inline screenshots and videos
|
|
1079
1004
|
|
|
1080
|
-
|
|
1005
|
+
GitLab supports native project Markdown uploads. Always upload the media to Replicas first, then POST the same original file to the repository's \`/projects/:id/uploads\` API using the workspace GitLab credential. Insert the returned \`markdown\` value into the merge request body or comment and include the Replicas dashboard link. Image uploads render inline; MP4, MOV, and WebM uploads render as inline video players.
|
|
1006
|
+
|
|
1007
|
+
Do not use a Replicas public forge share for GitLab when the native project upload succeeds; native uploads preserve project access controls.
|
|
1081
1008
|
|
|
1082
1009
|
## Recording defaults
|
|
1083
1010
|
|
|
@@ -1105,29 +1032,33 @@ Auto-detected from the filename extension:
|
|
|
1105
1032
|
| \`png\`, \`jpg\`, \`jpeg\`, \`webp\` | image |
|
|
1106
1033
|
| \`mp4\`, \`webm\` | video |
|
|
1107
1034
|
| \`mp3\`, \`wav\` | audio |
|
|
1035
|
+
| \`html\`, \`htm\` | html |
|
|
1108
1036
|
|
|
1109
|
-
For other extensions, pass \`--kind image|video|audio\` explicitly. The kind applies to every file in that invocation, so group files of the same kind together:
|
|
1037
|
+
For other extensions, pass \`--kind image|video|audio|html\` explicitly. The kind applies to every file in that invocation, so group files of the same kind together:
|
|
1110
1038
|
|
|
1111
1039
|
\`\`\`bash
|
|
1112
|
-
replicas media upload diagram.svg --kind image
|
|
1113
|
-
replicas media upload chart-a.svg chart-b.svg --kind image
|
|
1040
|
+
replicas media upload diagram.svg --name "Architecture diagram" --kind image
|
|
1041
|
+
replicas media upload chart-a.svg chart-b.svg --name "Latency chart" --name "Error rate chart" --kind image
|
|
1114
1042
|
\`\`\`
|
|
1115
1043
|
|
|
1116
1044
|
## Options
|
|
1117
1045
|
|
|
1118
|
-
- \`--kind <image|video|audio>\` \u2014 override auto-detection
|
|
1046
|
+
- \`--kind <image|video|audio|html>\` \u2014 override auto-detection
|
|
1047
|
+
- \`--name <name>\`: required display name shown in the dashboard, one per file in order
|
|
1119
1048
|
- \`--session-id <id>\` \u2014 associate the upload with a specific session
|
|
1049
|
+
- \`--access organization|public\`: create an organization-only link, or a public bearer URL for non-HTML media
|
|
1050
|
+
- \`--share\`: alias for \`--access public\`; not supported for HTML
|
|
1120
1051
|
`;
|
|
1121
1052
|
var MEDIA_ABILITY = {
|
|
1122
1053
|
label: "Media",
|
|
1123
|
-
description: "Share screenshots, recordings, generated images, and
|
|
1124
|
-
bullet: "- Producing or showing the user any media \u2014 screenshots, screen recordings, generated images or diagrams, audio clips \u2014 including in your Replicas chat reply, PR descriptions/comments, and other external platforms",
|
|
1125
|
-
section:
|
|
1126
|
-
referenceFile: { name: "MEDIA.md", content:
|
|
1054
|
+
description: "Share screenshots, recordings, generated images, audio clips, and HTML pages.",
|
|
1055
|
+
bullet: "- Producing or showing the user any media \u2014 screenshots, screen recordings, generated images or diagrams, audio clips, or HTML recap pages \u2014 including in your Replicas chat reply, PR descriptions/comments, and other external platforms",
|
|
1056
|
+
section: SECTION6,
|
|
1057
|
+
referenceFile: { name: "MEDIA.md", content: REFERENCE6 }
|
|
1127
1058
|
};
|
|
1128
1059
|
|
|
1129
1060
|
// ../shared/src/default-skills/replicas-agent/abilities/previews.ts
|
|
1130
|
-
var
|
|
1061
|
+
var SECTION7 = `### Previews
|
|
1131
1062
|
Expose locally running services (web apps, APIs, databases) as public preview URLs so humans can interact with them directly.
|
|
1132
1063
|
|
|
1133
1064
|
**Reference:** \`references/PREVIEWS.md\`
|
|
@@ -1136,30 +1067,24 @@ Use this when:
|
|
|
1136
1067
|
- You need to start a service that a human should view or interact with
|
|
1137
1068
|
- The task involves UI work that benefits from human review
|
|
1138
1069
|
- You are verifying frontend/backend integrations visually`;
|
|
1139
|
-
var
|
|
1070
|
+
var REFERENCE7 = `# Preview URLs
|
|
1140
1071
|
|
|
1141
1072
|
When you run services on ports \u2014 such as a web app, API server, or database \u2014 humans may want to interact with them directly. You can expose your locally running services as public preview URLs.
|
|
1142
1073
|
|
|
1143
1074
|
## Running Services for Preview
|
|
1144
1075
|
|
|
1145
|
-
|
|
1076
|
+
Always start services with \`replicas service start\` (see \`REPLICAS.md\`, "Long-running services"). It daemonizes the process so it survives your turn ending and workspace sleep/wake \u2014 a service left attached to your shell or backgrounded with \`&\`/\`nohup\` dies when the workspace sleeps, leaving the preview broken for the user.
|
|
1146
1077
|
|
|
1147
|
-
Some potential methods:
|
|
1148
1078
|
\`\`\`bash
|
|
1149
|
-
|
|
1150
|
-
setsid -f bash -lc 'cd /path/to/app && exec yarn dev >> /tmp/app.log 2>&1'
|
|
1151
|
-
|
|
1152
|
-
# For daemons like Docker
|
|
1153
|
-
nohup dockerd > /tmp/dockerd.log 2>&1 &
|
|
1079
|
+
replicas service start web "yarn dev" --cwd /path/to/app
|
|
1154
1080
|
\`\`\`
|
|
1155
1081
|
|
|
1156
1082
|
After starting a service:
|
|
1157
|
-
1.
|
|
1158
|
-
2.
|
|
1159
|
-
3.
|
|
1160
|
-
4. Only create the preview after the service is healthy
|
|
1083
|
+
1. Check logs for readiness: \`replicas service logs web\`
|
|
1084
|
+
2. Confirm it's actually serving: \`curl -s http://localhost:3000\` (or appropriate health check)
|
|
1085
|
+
3. Only create the preview after the service is healthy
|
|
1161
1086
|
|
|
1162
|
-
|
|
1087
|
+
\`replicas service start\` on the same name restarts the service, so you never need to hunt down stale processes on the port.
|
|
1163
1088
|
|
|
1164
1089
|
## Creating Previews
|
|
1165
1090
|
|
|
@@ -1218,8 +1143,149 @@ var PREVIEWS_ABILITY = {
|
|
|
1218
1143
|
label: "Previews",
|
|
1219
1144
|
description: "Expose locally running services on public preview URLs for humans.",
|
|
1220
1145
|
bullet: "- Creating preview URLs for locally running services",
|
|
1146
|
+
section: SECTION7,
|
|
1147
|
+
referenceFile: { name: "PREVIEWS.md", content: REFERENCE7 }
|
|
1148
|
+
};
|
|
1149
|
+
|
|
1150
|
+
// ../shared/src/default-skills/replicas-agent/abilities/plugins.ts
|
|
1151
|
+
var SECTION8 = `### Plugins
|
|
1152
|
+
Discover and execute installed marketplace tools through the workspace-scoped TypeScript SDK.
|
|
1153
|
+
|
|
1154
|
+
**Reference:** \`references/PLUGINS.md\`
|
|
1155
|
+
|
|
1156
|
+
Use this when:
|
|
1157
|
+
- The user asks for CRM data from Attio
|
|
1158
|
+
- The user asks to manage Calendly events, invitees, or scheduling links
|
|
1159
|
+
- The user asks to read or change Google Ads, Docs, Drive, Search Console, or Sheets data
|
|
1160
|
+
- The user asks for ClickHouse analytics or PostHog product data
|
|
1161
|
+
- The user asks to create or control an E2B sandbox
|
|
1162
|
+
- The user asks to invoke a connected Modal function
|
|
1163
|
+
- The user asks to query or update a connected turbopuffer namespace
|
|
1164
|
+
- The user asks for PlanetScale organizations, databases, branches, or Insights data
|
|
1165
|
+
- The user asks to analyze text with Pangram
|
|
1166
|
+
- The user asks for Stripe customers, payments, invoices, subscriptions, or balances`;
|
|
1167
|
+
var REFERENCE8 = `# Plugins
|
|
1168
|
+
|
|
1169
|
+
Plugins are TypeScript libraries, not MCP servers. Import \`@replicas/sdk\`; Replicas authenticates the workspace and selects only the owner\u2019s personal connection or the organization fallback. Provider credentials, sessions, and project keys are never exposed.
|
|
1170
|
+
|
|
1171
|
+
Plugin tools may read or write provider data according to the connected account's permissions. Only execute write or destructive tools when they match the user's request.
|
|
1172
|
+
|
|
1173
|
+
## Discover installed plugins
|
|
1174
|
+
|
|
1175
|
+
\`\`\`ts
|
|
1176
|
+
import { replicas } from '@replicas/sdk';
|
|
1177
|
+
|
|
1178
|
+
const plugins = await replicas.plugins.list();
|
|
1179
|
+
const installed = plugins.filter((plugin) => plugin.connected);
|
|
1180
|
+
\`\`\`
|
|
1181
|
+
|
|
1182
|
+
If a plugin is not connected, ask the user to install it under personal or organization Integrations in the Replicas dashboard.
|
|
1183
|
+
|
|
1184
|
+
## Search, inspect, execute
|
|
1185
|
+
|
|
1186
|
+
\`\`\`ts
|
|
1187
|
+
const search = await replicas.plugins.search({
|
|
1188
|
+
plugin: 'stripe',
|
|
1189
|
+
query: 'list recent successful payments with customer details',
|
|
1190
|
+
});
|
|
1191
|
+
|
|
1192
|
+
const [tool] = search.tools;
|
|
1193
|
+
if (!tool) throw new Error('No matching tool found');
|
|
1194
|
+
|
|
1195
|
+
const { data } = await replicas.plugins.execute({
|
|
1196
|
+
plugin: 'stripe',
|
|
1197
|
+
tool: tool.slug,
|
|
1198
|
+
arguments: { limit: 25 },
|
|
1199
|
+
});
|
|
1200
|
+
\`\`\`
|
|
1201
|
+
|
|
1202
|
+
Use \`replicas.plugins.describe({ plugin, tools: [slug] })\` when search returned a partial schema or before constructing unfamiliar arguments.
|
|
1203
|
+
|
|
1204
|
+
## Batch work
|
|
1205
|
+
|
|
1206
|
+
Keep dependent operations in one TypeScript file and batch independent calls:
|
|
1207
|
+
|
|
1208
|
+
\`\`\`ts
|
|
1209
|
+
const results = await Promise.all(customerIds.map((customer) =>
|
|
1210
|
+
replicas.plugins.execute({
|
|
1211
|
+
plugin: 'stripe',
|
|
1212
|
+
tool: 'STRIPE_RETRIEVE_CUSTOMER',
|
|
1213
|
+
arguments: { customer },
|
|
1214
|
+
}),
|
|
1215
|
+
));
|
|
1216
|
+
\`\`\`
|
|
1217
|
+
|
|
1218
|
+
Never attempt to import Composio, create a session, manage a connection, or call a provider with raw credentials. Those operations are intentionally unavailable inside the workspace.
|
|
1219
|
+
|
|
1220
|
+
## PlanetScale
|
|
1221
|
+
|
|
1222
|
+
PlanetScale is a native OAuth integration. Pass a relative path from the PlanetScale v1 API; Replicas refreshes access tokens server-side:
|
|
1223
|
+
|
|
1224
|
+
\`\`\`ts
|
|
1225
|
+
const organizations = await replicas.planetscale.request('/organizations');
|
|
1226
|
+
const databases = await replicas.planetscale.request('/organizations/acme/databases');
|
|
1227
|
+
\`\`\`
|
|
1228
|
+
|
|
1229
|
+
OAuth scopes control permitted operations. Confirm mutations before using POST, PUT, PATCH, or DELETE.
|
|
1230
|
+
|
|
1231
|
+
## Modal
|
|
1232
|
+
|
|
1233
|
+
Modal is a native integration and uses its official JavaScript SDK behind the Replicas gateway. Invoke a deployed function by name:
|
|
1234
|
+
|
|
1235
|
+
\`\`\`ts
|
|
1236
|
+
const result = await replicas.modal.call({
|
|
1237
|
+
app: 'my-app',
|
|
1238
|
+
function: 'my-function',
|
|
1239
|
+
args: ['input'],
|
|
1240
|
+
environment: 'main',
|
|
1241
|
+
});
|
|
1242
|
+
\`\`\`
|
|
1243
|
+
|
|
1244
|
+
Function calls execute with the connected Modal token and may mutate external state. Confirm the requested action before invoking a function with side effects.
|
|
1245
|
+
|
|
1246
|
+
## turbopuffer
|
|
1247
|
+
|
|
1248
|
+
turbopuffer is a native integration backed by its official TypeScript SDK. Use the provider-shaped helpers:
|
|
1249
|
+
|
|
1250
|
+
\`\`\`ts
|
|
1251
|
+
const namespaces = await replicas.turbopuffer.namespaces({ prefix: 'products' });
|
|
1252
|
+
const results = await replicas.turbopuffer.query('products', {
|
|
1253
|
+
rank_by: ['text', 'BM25', 'wireless headphones'],
|
|
1254
|
+
top_k: 10,
|
|
1255
|
+
});
|
|
1256
|
+
\`\`\`
|
|
1257
|
+
|
|
1258
|
+
\`write\` mutates namespace data. Confirm the requested change before calling it.
|
|
1259
|
+
|
|
1260
|
+
## Pangram
|
|
1261
|
+
|
|
1262
|
+
Pangram is a native integration. Text sent to either method is processed by Pangram and consumes the connected account's credits:
|
|
1263
|
+
|
|
1264
|
+
\`\`\`ts
|
|
1265
|
+
const detection = await replicas.pangram.detect({ text });
|
|
1266
|
+
const plagiarism = await replicas.pangram.plagiarism(text);
|
|
1267
|
+
\`\`\`
|
|
1268
|
+
|
|
1269
|
+
Do not send private or sensitive text without the user's authorization.
|
|
1270
|
+
|
|
1271
|
+
## E2B
|
|
1272
|
+
|
|
1273
|
+
E2B is a native integration backed by its official JavaScript SDK. Create a sandbox, run commands, and remove it when finished:
|
|
1274
|
+
|
|
1275
|
+
\`\`\`ts
|
|
1276
|
+
const { sandboxId } = await replicas.e2b.create({ timeoutMs: 300_000 });
|
|
1277
|
+
const result = await replicas.e2b.run(sandboxId, 'python analysis.py', { cwd: '/home/user' });
|
|
1278
|
+
await replicas.e2b.kill(sandboxId);
|
|
1279
|
+
\`\`\`
|
|
1280
|
+
|
|
1281
|
+
Sandbox operations consume the connected account's E2B credits. Kill sandboxes after use unless the user asked to keep one running.
|
|
1282
|
+
`;
|
|
1283
|
+
var PLUGINS_ABILITY = {
|
|
1284
|
+
label: "Plugins",
|
|
1285
|
+
description: "Use installed marketplace tools through @replicas/sdk.",
|
|
1286
|
+
bullet: "- Interacting with installed marketplace plugins through TypeScript",
|
|
1221
1287
|
section: SECTION8,
|
|
1222
|
-
referenceFile: { name: "
|
|
1288
|
+
referenceFile: { name: "PLUGINS.md", content: REFERENCE8 }
|
|
1223
1289
|
};
|
|
1224
1290
|
|
|
1225
1291
|
// ../shared/src/default-skills/replicas-agent/abilities/replicas.ts
|
|
@@ -1232,7 +1298,8 @@ Use this when:
|
|
|
1232
1298
|
- The user asks you to create, edit, run, or delete an automation
|
|
1233
1299
|
- The user asks you to manage environments, environment variables, or environment files
|
|
1234
1300
|
- The user asks "what envs / repos / automations do I have?"
|
|
1235
|
-
- The user asks you to scaffold a \`replicas.json\` / \`replicas.yaml\` in a repo
|
|
1301
|
+
- The user asks you to scaffold a \`replicas.json\` / \`replicas.yaml\` in a repo
|
|
1302
|
+
- You need to run a long-lived service (dev server, daemon) \u2014 always use \`replicas service start\` so it survives workspace sleep/wake`;
|
|
1236
1303
|
var REFERENCE9 = `# Replicas (in-workspace CLI)
|
|
1237
1304
|
|
|
1238
1305
|
This guide covers how to take action *with* Replicas itself from inside a Replicas workspace \u2014 managing automations, environments (and their variables/files), repos, previews, and the user's \`replicas.json\` config \u2014 using the pre-installed \`replicas\` CLI.
|
|
@@ -1268,7 +1335,9 @@ In agent mode the CLI hides commands that don't make sense for in-workspace agen
|
|
|
1268
1335
|
| \`replicas environment ...\` | Manage environments, env vars, env files |
|
|
1269
1336
|
| \`replicas automation ...\` | Manage automations (cron + GitHub/GitLab event triggers) |
|
|
1270
1337
|
| \`replicas preview ...\` | Register / list preview URLs (covered in \`PREVIEWS.md\`) |
|
|
1338
|
+
| \`replicas service ...\` | Run long-lived services detached so they survive workspace sleep/wake (see below) |
|
|
1271
1339
|
| \`replicas media ...\` | Upload screenshots, videos, audio (covered in \`MEDIA.md\`) |
|
|
1340
|
+
| \`replicas slack thread ...\` | Attach or switch Slack thread routing (covered in \`SLACK.md\`) |
|
|
1272
1341
|
|
|
1273
1342
|
\`replicas <command> --help\` is always the source of truth for flags.
|
|
1274
1343
|
|
|
@@ -1285,6 +1354,7 @@ In agent mode the CLI hides commands that don't make sense for in-workspace agen
|
|
|
1285
1354
|
| "Make an automation that runs every weekday at 9am" | \`replicas automation create\` (interactive) or \`--trigger-cron "0 9 * * 1-5"\` |
|
|
1286
1355
|
| "Make an automation that runs when a PR opens on \`acme/web\`" | \`replicas automation create ... --trigger-github pull_request.opened --trigger-github-repos acme/web\` |
|
|
1287
1356
|
| "Make an automation that runs when an MR opens on \`acme/web\`" | \`replicas automation create ... --trigger-gitlab merge_request.opened --trigger-gitlab-repos acme/web\` |
|
|
1357
|
+
| "Ignore Dependabot for this PR automation" | \`replicas automation edit <id> --trigger-github-exclude-users 'dependabot[bot]'\` |
|
|
1288
1358
|
| "What repos are connected?" | \`replicas repos\` |
|
|
1289
1359
|
| "Set up a \`replicas.json\` in this repo" | \`replicas init\` (\`-y\` for YAML) |
|
|
1290
1360
|
|
|
@@ -1371,13 +1441,15 @@ replicas automation create "Review my PRs" \\
|
|
|
1371
1441
|
--prompt "Leave a code review on this PR" \\
|
|
1372
1442
|
--environment <env-name-or-id> \\
|
|
1373
1443
|
--trigger-github pull_request.opened \\
|
|
1374
|
-
--trigger-github-repos acme/web,acme/api
|
|
1444
|
+
--trigger-github-repos acme/web,acme/api \\
|
|
1445
|
+
--trigger-github-exclude-users 'dependabot[bot],renovate[bot]'
|
|
1375
1446
|
|
|
1376
1447
|
replicas automation create "Review my MRs" \\
|
|
1377
1448
|
--prompt "Leave a code review on this MR" \\
|
|
1378
1449
|
--environment <env-name-or-id> \\
|
|
1379
1450
|
--trigger-gitlab merge_request.opened \\
|
|
1380
|
-
--trigger-gitlab-repos acme/web,acme/api
|
|
1451
|
+
--trigger-gitlab-repos acme/web,acme/api \\
|
|
1452
|
+
--trigger-gitlab-exclude-users 'bot-user'
|
|
1381
1453
|
|
|
1382
1454
|
# Disable on creation
|
|
1383
1455
|
replicas automation create ... --disabled
|
|
@@ -1385,7 +1457,7 @@ replicas automation create ... --disabled
|
|
|
1385
1457
|
# Workspace lifecycle
|
|
1386
1458
|
replicas automation create ... --lifecycle archive_when_done
|
|
1387
1459
|
replicas automation create ... --lifecycle sleep_when_done
|
|
1388
|
-
replicas automation create ... --lifecycle
|
|
1460
|
+
replicas automation create ... --lifecycle default --auto-stop-minutes 30
|
|
1389
1461
|
\`\`\`
|
|
1390
1462
|
|
|
1391
1463
|
When the user says "make me an automation that...", default to **picking the environment for them by listing the available envs** and asking only when the user hasn't already pinned one. Same for repos in GitHub triggers. Don't pepper the user with questions you can answer by reading the existing config.
|
|
@@ -1401,8 +1473,28 @@ replicas automation edit <id> \\
|
|
|
1401
1473
|
--trigger-cron "0 4 * * *" # replaces existing triggers
|
|
1402
1474
|
\`\`\`
|
|
1403
1475
|
|
|
1476
|
+
\`replicas automation edit <id> --trigger-github-exclude-users "dependabot[bot],renovate[bot]"\` updates existing GitHub triggers without replacing the rest of the trigger list. The GitLab equivalent is \`--trigger-gitlab-exclude-users\`. Passing an empty string (for example, \`--trigger-github-exclude-users ""\`) clears existing exclusions for that code host. Passing \`--trigger-github\` or \`--trigger-gitlab\` still replaces triggers, and the matching exclude flag is applied to the replacement trigger.
|
|
1477
|
+
|
|
1404
1478
|
\`replicas automation edit <id>\` with no flags drops into interactive mode.
|
|
1405
1479
|
|
|
1480
|
+
## Long-running services
|
|
1481
|
+
|
|
1482
|
+
**Always use \`replicas service start\` to run anything that should keep running after your current command or turn ends** \u2014 dev servers, backend APIs, databases, daemons, watchers. Never leave them attached to your shell, and never rely on plain \`&\`, \`nohup\`, or your own backgrounding: processes started inside your session are torn down when your turn ends and the workspace goes to sleep, so the user finds them dead after waking the workspace. \`replicas service start\` daemonizes the process into its own session, which survives workspace sleep/wake.
|
|
1483
|
+
|
|
1484
|
+
\`\`\`bash
|
|
1485
|
+
replicas service start <name> "<command>" # start (or restart) a named service
|
|
1486
|
+
replicas service start web "bun run dev" --cwd ~/workspaces/app
|
|
1487
|
+
replicas service list # names, pids, running/stopped
|
|
1488
|
+
replicas service logs <name> [-n 100] # tail the service log
|
|
1489
|
+
replicas service stop <name> # stop the whole process group
|
|
1490
|
+
\`\`\`
|
|
1491
|
+
|
|
1492
|
+
Notes:
|
|
1493
|
+
- Quote the command if it contains shell operators: \`replicas service start web "cd app && bun dev"\`.
|
|
1494
|
+
- \`start\` on an existing name restarts it (the old process group is stopped first). It fails fast and prints the log tail if the service dies within the first second.
|
|
1495
|
+
- Logs stream to \`~/.replicas/services/<name>.log\`.
|
|
1496
|
+
- After starting, verify the service is actually healthy (check \`logs\`, then \`curl\` its port) before telling the user it's up or creating a preview for it.
|
|
1497
|
+
|
|
1406
1498
|
## Repositories
|
|
1407
1499
|
|
|
1408
1500
|
Read-only listing of repos connected to the org. Use when the user asks "what repos can I use?", or to validate a \`--repository\` value before passing it to \`environment create\` / \`automation create\`:
|
|
@@ -1440,120 +1532,123 @@ var REPLICAS_ABILITY = {
|
|
|
1440
1532
|
locked: true
|
|
1441
1533
|
};
|
|
1442
1534
|
|
|
1443
|
-
// ../shared/src/default-skills/replicas-agent/abilities/
|
|
1444
|
-
var SECTION10 = `###
|
|
1445
|
-
|
|
1535
|
+
// ../shared/src/default-skills/replicas-agent/abilities/sentry.ts
|
|
1536
|
+
var SECTION10 = `### Sentry
|
|
1537
|
+
Read issues and project data through the workspace-scoped TypeScript SDK.
|
|
1446
1538
|
|
|
1447
|
-
**Reference:** \`references/
|
|
1539
|
+
**Reference:** \`references/SENTRY.md\`
|
|
1448
1540
|
|
|
1449
1541
|
Use this when:
|
|
1450
|
-
- You need to
|
|
1451
|
-
-
|
|
1452
|
-
|
|
1453
|
-
- The task asks you to notify, update, or communicate via Slack`;
|
|
1454
|
-
var REFERENCE10 = `# Slack Integration
|
|
1542
|
+
- You need to inspect Sentry projects or issues
|
|
1543
|
+
- A user asks for error-monitoring context outside a webhook-triggered task`;
|
|
1544
|
+
var REFERENCE10 = `# Sentry Integration
|
|
1455
1545
|
|
|
1456
|
-
|
|
1546
|
+
Use \`@replicas/sdk\`; never request or read a Sentry token. Sentry access is organization-scoped and read-only.
|
|
1457
1547
|
|
|
1458
|
-
|
|
1548
|
+
\`\`\`ts
|
|
1549
|
+
import { replicas } from '@replicas/sdk';
|
|
1459
1550
|
|
|
1460
|
-
|
|
1551
|
+
const { native } = await replicas.integrations.list();
|
|
1552
|
+
if (!native.sentry) throw new Error('Connect Sentry in Replicas organization integrations first.');
|
|
1461
1553
|
|
|
1462
|
-
|
|
1463
|
-
echo "\${SLACK_BOT_TOKEN:+set}"
|
|
1554
|
+
const issues = await replicas.sentry.request('/organizations/acme/issues/?query=is:unresolved');
|
|
1464
1555
|
\`\`\`
|
|
1465
1556
|
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1557
|
+
Pass only a relative Sentry API path. The Replicas gateway selects the connected organization credential and rejects writes, absolute URLs, oversized responses, and long-running requests.
|
|
1558
|
+
`;
|
|
1559
|
+
var SENTRY_ABILITY = {
|
|
1560
|
+
label: "Sentry",
|
|
1561
|
+
description: "Read Sentry projects and issues through @replicas/sdk.",
|
|
1562
|
+
bullet: "- Interacting with Sentry (reading projects, issues, and error context)",
|
|
1563
|
+
section: SECTION10,
|
|
1564
|
+
referenceFile: { name: "SENTRY.md", content: REFERENCE10 }
|
|
1565
|
+
};
|
|
1470
1566
|
|
|
1471
|
-
|
|
1567
|
+
// ../shared/src/default-skills/replicas-agent/abilities/slack.ts
|
|
1568
|
+
var SECTION11 = `### Slack
|
|
1569
|
+
Send messages, read threads, search conversations, and upload files through the workspace-scoped TypeScript SDK.
|
|
1472
1570
|
|
|
1473
|
-
|
|
1571
|
+
**Reference:** \`references/SLACK.md\`
|
|
1474
1572
|
|
|
1475
|
-
|
|
1573
|
+
Use this when:
|
|
1574
|
+
- You need to send a message to a Slack channel or thread
|
|
1575
|
+
- You need to read or search Slack conversations
|
|
1576
|
+
- You encounter a Slack message link and need its full thread`;
|
|
1577
|
+
var REFERENCE11 = `# Slack Integration
|
|
1476
1578
|
|
|
1477
|
-
|
|
1478
|
-
- **Thread TS**: The \`p\` value with a dot inserted before the last 6 digits (e.g. \`p1234567890123456\` -> \`1234567890.123456\`)
|
|
1579
|
+
Use \`@replicas/sdk\`; never read or send a Slack token directly. The SDK calls Replicas, which selects only this workspace's organization connection.
|
|
1479
1580
|
|
|
1480
|
-
|
|
1481
|
-
curl -s "https://slack.com/api/conversations.replies?channel=CHANNEL_ID&ts=THREAD_TS" \\
|
|
1482
|
-
-H "Authorization: Bearer $SLACK_BOT_TOKEN"
|
|
1483
|
-
\`\`\`
|
|
1581
|
+
## Check access
|
|
1484
1582
|
|
|
1485
|
-
|
|
1583
|
+
\`\`\`ts
|
|
1584
|
+
import { replicas } from '@replicas/sdk';
|
|
1486
1585
|
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
-H "Authorization: Bearer $SLACK_BOT_TOKEN" \\
|
|
1490
|
-
-H "Content-Type: application/json" \\
|
|
1491
|
-
-d '{
|
|
1492
|
-
"channel": "CHANNEL_ID",
|
|
1493
|
-
"text": "Your message here",
|
|
1494
|
-
"thread_ts": "OPTIONAL_THREAD_TS"
|
|
1495
|
-
}'
|
|
1586
|
+
const { native } = await replicas.integrations.list();
|
|
1587
|
+
if (!native.slack) throw new Error('Connect Slack in Replicas organization integrations first.');
|
|
1496
1588
|
\`\`\`
|
|
1497
1589
|
|
|
1498
|
-
|
|
1590
|
+
## Read a thread
|
|
1499
1591
|
|
|
1500
|
-
|
|
1592
|
+
For a link such as \`https://team.slack.com/archives/C0123ABC/p1234567890123456\`, the channel is \`C0123ABC\` and the timestamp is \`1234567890.123456\`.
|
|
1501
1593
|
|
|
1502
|
-
\`\`\`
|
|
1503
|
-
|
|
1504
|
-
|
|
1594
|
+
\`\`\`ts
|
|
1595
|
+
const thread = await replicas.slack.call('conversations.replies', {
|
|
1596
|
+
channel: 'C0123ABC',
|
|
1597
|
+
ts: '1234567890.123456',
|
|
1598
|
+
limit: 200,
|
|
1599
|
+
});
|
|
1505
1600
|
\`\`\`
|
|
1506
1601
|
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
Slack deprecated \`files.upload\`; the raw HTTP replacement is a 3-step flow. (\`files.uploadV2\` is **only** an SDK convenience wrapper and is NOT a real Web API HTTP endpoint \u2014 \`POST https://slack.com/api/files.uploadV2\` returns \`unknown_method\`.)
|
|
1510
|
-
|
|
1511
|
-
\`\`\`bash
|
|
1512
|
-
FILE=/path/to/file
|
|
1513
|
-
FILENAME=$(basename "$FILE")
|
|
1514
|
-
LENGTH=$(stat -c%s "$FILE" 2>/dev/null || stat -f%z "$FILE")
|
|
1602
|
+
## Send and attach a message
|
|
1515
1603
|
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1604
|
+
\`\`\`ts
|
|
1605
|
+
const message = await replicas.slack.call<{ channel: string; ts: string }>('chat.postMessage', {
|
|
1606
|
+
channel: 'C0123ABC',
|
|
1607
|
+
text: 'Update from Replicas',
|
|
1608
|
+
});
|
|
1609
|
+
await replicas.slack.attachThread({ channel: message.channel, threadTs: message.ts });
|
|
1610
|
+
\`\`\`
|
|
1523
1611
|
|
|
1524
|
-
|
|
1525
|
-
# Do not use -F / multipart here \u2014 the presigned URL expects the bytes
|
|
1526
|
-
# directly in the request body.
|
|
1527
|
-
curl -s -X POST "$UPLOAD_URL" \\
|
|
1528
|
-
-H "Content-Type: application/octet-stream" \\
|
|
1529
|
-
--data-binary @"$FILE"
|
|
1612
|
+
A link in the message text stays plain text, so the message that first shares a pull request or merge request link passes \`blocks\` alongside \`text\` to render a button for it. Keep \`text\` set to the same message \u2014 Slack uses it for notifications and fallback.
|
|
1530
1613
|
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1614
|
+
\`\`\`ts
|
|
1615
|
+
await replicas.slack.call('chat.postMessage', {
|
|
1616
|
+
channel: 'C0123ABC',
|
|
1617
|
+
thread_ts: '1234567890.123456',
|
|
1618
|
+
text: summary,
|
|
1619
|
+
blocks: [
|
|
1620
|
+
{ type: 'section', text: { type: 'mrkdwn', text: summary } },
|
|
1621
|
+
{
|
|
1622
|
+
type: 'actions',
|
|
1623
|
+
elements: [
|
|
1624
|
+
{ type: 'button', style: 'primary', text: { type: 'plain_text', text: 'View PR' }, url: prUrl, action_id: 'view_pr' },
|
|
1625
|
+
],
|
|
1626
|
+
},
|
|
1627
|
+
],
|
|
1628
|
+
});
|
|
1541
1629
|
\`\`\`
|
|
1542
1630
|
|
|
1543
|
-
|
|
1631
|
+
Only the announcing message gets the button \u2014 follow-ups about a pull request already linked in the thread keep the link in the text and drop the \`blocks\`. Use "View MR" for GitLab. With several links, give each button a unique \`action_id\` and name its repository in the label ("View PR \xB7 web").
|
|
1632
|
+
|
|
1633
|
+
Attach every new top-level conversation so future replies route to this workspace. For a reply in another thread, attach its root \`thread_ts\`, not the reply timestamp. Do not attach the originating thread again.
|
|
1544
1634
|
|
|
1545
|
-
|
|
1635
|
+
## Search and batch
|
|
1546
1636
|
|
|
1547
|
-
|
|
1637
|
+
\`\`\`ts
|
|
1638
|
+
const [messages, users] = await Promise.all([
|
|
1639
|
+
replicas.slack.call('search.messages', { query: 'launch status' }),
|
|
1640
|
+
replicas.slack.call('users.list', { limit: 200 }),
|
|
1641
|
+
]);
|
|
1642
|
+
\`\`\`
|
|
1548
1643
|
|
|
1549
|
-
|
|
1644
|
+
Supported methods cover conversations, messages, search, users, reactions, and Slack's external file-upload flow. An unsupported method is rejected by Replicas before Slack is called.
|
|
1550
1645
|
`;
|
|
1551
1646
|
var SLACK_ABILITY = {
|
|
1552
1647
|
label: "Slack",
|
|
1553
|
-
description: "Send messages, read threads, search
|
|
1648
|
+
description: "Send messages, read threads, and search through @replicas/sdk.",
|
|
1554
1649
|
bullet: "- Interacting with Slack (sending messages, reading threads, etc.)",
|
|
1555
|
-
section:
|
|
1556
|
-
referenceFile: { name: "SLACK.md", content:
|
|
1650
|
+
section: SECTION11,
|
|
1651
|
+
referenceFile: { name: "SLACK.md", content: REFERENCE11 }
|
|
1557
1652
|
};
|
|
1558
1653
|
|
|
1559
1654
|
// ../shared/src/default-skills/replicas-agent/registry.ts
|
|
@@ -1563,10 +1658,11 @@ var REPLICAS_AGENT_ABILITY_REGISTRY = {
|
|
|
1563
1658
|
docker: DOCKER_ABILITY,
|
|
1564
1659
|
github: GITHUB_ABILITY,
|
|
1565
1660
|
gitlab: GITLAB_ABILITY,
|
|
1566
|
-
google: GOOGLE_ABILITY,
|
|
1567
1661
|
linear: LINEAR_ABILITY,
|
|
1568
1662
|
media: MEDIA_ABILITY,
|
|
1663
|
+
plugins: PLUGINS_ABILITY,
|
|
1569
1664
|
previews: PREVIEWS_ABILITY,
|
|
1665
|
+
sentry: SENTRY_ABILITY,
|
|
1570
1666
|
slack: SLACK_ABILITY
|
|
1571
1667
|
};
|
|
1572
1668
|
var REPLICAS_AGENT_ABILITIES = Object.keys(
|
|
@@ -1574,36 +1670,323 @@ var REPLICAS_AGENT_ABILITIES = Object.keys(
|
|
|
1574
1670
|
);
|
|
1575
1671
|
|
|
1576
1672
|
// ../shared/src/routes/preferences.ts
|
|
1577
|
-
var
|
|
1578
|
-
|
|
1579
|
-
|
|
1673
|
+
var EMPTY_AGENT_DEFAULT_SETTINGS = {
|
|
1674
|
+
agent_provider: null,
|
|
1675
|
+
model: null,
|
|
1676
|
+
thinking_level: null,
|
|
1677
|
+
goal_mode: null,
|
|
1678
|
+
fast_mode: null
|
|
1679
|
+
};
|
|
1680
|
+
var DEFAULT_RELAY_SUBAGENT_PROVIDERS = [...VALID_RELAY_SUBAGENT_PROVIDERS];
|
|
1681
|
+
var DEFAULT_USER_PREFERENCES = {
|
|
1682
|
+
default_agent: "codex",
|
|
1683
|
+
default_model: null,
|
|
1684
|
+
claude_models: null,
|
|
1685
|
+
codex_models: null,
|
|
1686
|
+
cursor_models: null,
|
|
1687
|
+
openrouter_models: null,
|
|
1688
|
+
relay_models: null,
|
|
1689
|
+
close_workspace_on_pr_close: true,
|
|
1690
|
+
close_workspace_on_pr_merge: true,
|
|
1691
|
+
default_environment_id: null,
|
|
1692
|
+
respond_to_ci_workflows: true,
|
|
1693
|
+
respond_to_ci_checks: true,
|
|
1694
|
+
respond_to_human_pr_reviews: false,
|
|
1695
|
+
pr_user_attribution: false,
|
|
1696
|
+
auto_draft_prs: false,
|
|
1697
|
+
open_prs_in_graphite: false,
|
|
1698
|
+
open_links_in_desktop_app: false,
|
|
1699
|
+
default_fast_mode: false,
|
|
1700
|
+
agent_defaults: { ...EMPTY_AGENT_DEFAULT_SETTINGS },
|
|
1701
|
+
credential_priority: null
|
|
1702
|
+
};
|
|
1703
|
+
function defaultReplicasAgentAbilities() {
|
|
1704
|
+
const abilities = {};
|
|
1705
|
+
for (const key of REPLICAS_AGENT_ABILITIES) {
|
|
1706
|
+
abilities[key] = true;
|
|
1707
|
+
}
|
|
1708
|
+
if (hasAllReplicasAgentAbilities(abilities)) return abilities;
|
|
1709
|
+
throw new Error("Default Replicas agent ability flags are incomplete");
|
|
1710
|
+
}
|
|
1711
|
+
function hasAllReplicasAgentAbilities(abilities) {
|
|
1712
|
+
return REPLICAS_AGENT_ABILITIES.every((key) => typeof abilities[key] === "boolean");
|
|
1713
|
+
}
|
|
1580
1714
|
var DEFAULT_DEFAULT_SKILLS = {
|
|
1581
1715
|
replicas_agent: {
|
|
1582
1716
|
enabled: true,
|
|
1583
|
-
abilities:
|
|
1717
|
+
abilities: defaultReplicasAgentAbilities()
|
|
1584
1718
|
}
|
|
1585
1719
|
};
|
|
1586
1720
|
|
|
1587
|
-
// ../shared/src/
|
|
1588
|
-
var
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1721
|
+
// ../shared/src/aster.ts
|
|
1722
|
+
var ASTER_MODELS = [
|
|
1723
|
+
"kimi-k3",
|
|
1724
|
+
"glm-5.2",
|
|
1725
|
+
"gpt-oss-120b-fast",
|
|
1726
|
+
"gpt-oss-120b"
|
|
1727
|
+
];
|
|
1728
|
+
var DEFAULT_ASTER_MODEL = ASTER_MODELS[0];
|
|
1729
|
+
var ASTER_MODEL_LABELS = {
|
|
1730
|
+
"kimi-k3": "Kimi K3",
|
|
1731
|
+
"glm-5.2": "GLM 5.2",
|
|
1732
|
+
"gpt-oss-120b-fast": "GPT-OSS 120B Fast",
|
|
1733
|
+
"gpt-oss-120b": "GPT-OSS 120B"
|
|
1734
|
+
};
|
|
1735
|
+
|
|
1736
|
+
// ../shared/src/engine/types.ts
|
|
1737
|
+
var CLAUDE_OPUS_1M_MODEL = "opus[1m]";
|
|
1738
|
+
var LEGACY_CLAUDE_OPUS_1M_MODEL = "opus-1m";
|
|
1739
|
+
var CLAUDE_FABLE_5_MODEL = "claude-fable-5";
|
|
1740
|
+
var CLAUDE_OPUS_5_MODEL = "claude-opus-5";
|
|
1741
|
+
var CLAUDE_OPUS_4_8_MODEL = "claude-opus-4-8";
|
|
1742
|
+
var CLAUDE_SONNET_5_MODEL = "claude-sonnet-5";
|
|
1743
|
+
var CLAUDE_HAIKU_4_5_MODEL = "claude-haiku-4-5";
|
|
1744
|
+
var CLAUDE_FABLE_5_BEDROCK_MODEL = "anthropic.claude-fable-5";
|
|
1745
|
+
var CLAUDE_OPUS_5_BEDROCK_MODEL = "anthropic.claude-opus-5";
|
|
1746
|
+
var CLAUDE_OPUS_4_8_BEDROCK_MODEL = "us.anthropic.claude-opus-4-8";
|
|
1747
|
+
var CLAUDE_SONNET_5_BEDROCK_MODEL = "anthropic.claude-sonnet-5";
|
|
1748
|
+
var CLAUDE_HAIKU_4_5_BEDROCK_MODEL = "us.anthropic.claude-haiku-4-5-20251001-v1:0";
|
|
1749
|
+
var GPT_5_6_SOL_MODEL = "gpt-5.6-sol";
|
|
1750
|
+
var GPT_5_6_TERRA_MODEL = "gpt-5.6-terra";
|
|
1751
|
+
var GPT_5_6_LUNA_MODEL = "gpt-5.6-luna";
|
|
1752
|
+
var DEFAULT_CODEX_MODEL = GPT_5_6_SOL_MODEL;
|
|
1753
|
+
var DEFAULT_OPENCODE_MODEL = "z-ai/glm-5.2";
|
|
1754
|
+
var DEEPSEEK_V4_PRO_MODEL = "deepseek/deepseek-v4-pro-0813";
|
|
1755
|
+
var DEEPSEEK_V4_FLASH_MODEL = "deepseek/deepseek-v4-flash-0731";
|
|
1756
|
+
var DEFAULT_FX_MODEL = "zai/glm-5.2";
|
|
1757
|
+
var OPENROUTER_MODELS = [
|
|
1758
|
+
DEFAULT_OPENCODE_MODEL,
|
|
1759
|
+
DEEPSEEK_V4_PRO_MODEL,
|
|
1760
|
+
DEEPSEEK_V4_FLASH_MODEL,
|
|
1761
|
+
"minimax/minimax-m3",
|
|
1762
|
+
"xiaomi/mimo-v2.5-pro",
|
|
1763
|
+
"moonshotai/kimi-k2.6"
|
|
1764
|
+
];
|
|
1765
|
+
function prioritizeDeepseekModels(models) {
|
|
1766
|
+
return [
|
|
1767
|
+
...models.filter((model) => model.startsWith("deepseek/")),
|
|
1768
|
+
...models.filter((model) => !model.startsWith("deepseek/"))
|
|
1769
|
+
];
|
|
1770
|
+
}
|
|
1771
|
+
var CLAUDE_MODEL_CONTEXT_WINDOWS = {
|
|
1772
|
+
[CLAUDE_FABLE_5_MODEL]: 1e6,
|
|
1773
|
+
[CLAUDE_OPUS_5_MODEL]: 1e6,
|
|
1774
|
+
[CLAUDE_OPUS_4_8_MODEL]: 1e6,
|
|
1775
|
+
[CLAUDE_SONNET_5_MODEL]: 1e6,
|
|
1776
|
+
[CLAUDE_HAIKU_4_5_MODEL]: 2e5,
|
|
1777
|
+
[CLAUDE_FABLE_5_BEDROCK_MODEL]: 1e6,
|
|
1778
|
+
[CLAUDE_OPUS_5_BEDROCK_MODEL]: 1e6,
|
|
1779
|
+
[CLAUDE_OPUS_4_8_BEDROCK_MODEL]: 1e6,
|
|
1780
|
+
[CLAUDE_SONNET_5_BEDROCK_MODEL]: 1e6,
|
|
1781
|
+
[CLAUDE_HAIKU_4_5_BEDROCK_MODEL]: 2e5,
|
|
1782
|
+
"anthropic.claude-opus-4-8": 1e6,
|
|
1783
|
+
"us.anthropic.claude-sonnet-5": 1e6,
|
|
1784
|
+
"anthropic.claude-haiku-4-5": 2e5,
|
|
1785
|
+
"claude-haiku-4-5-20251001": 2e5,
|
|
1786
|
+
"claude-haiku-4-5@20251001": 2e5
|
|
1787
|
+
};
|
|
1788
|
+
var AGENT_MODELS = {
|
|
1789
|
+
claude: [
|
|
1790
|
+
CLAUDE_FABLE_5_MODEL,
|
|
1791
|
+
CLAUDE_OPUS_5_MODEL,
|
|
1792
|
+
CLAUDE_OPUS_4_8_MODEL,
|
|
1793
|
+
CLAUDE_SONNET_5_MODEL,
|
|
1794
|
+
CLAUDE_HAIKU_4_5_MODEL
|
|
1795
|
+
],
|
|
1796
|
+
codex: [DEFAULT_CODEX_MODEL, GPT_5_6_TERRA_MODEL, GPT_5_6_LUNA_MODEL, "gpt-5.5", "gpt-5.4", "gpt-5.4-mini", "gpt-5.2"],
|
|
1797
|
+
cursor: [
|
|
1798
|
+
CLAUDE_FABLE_5_MODEL,
|
|
1799
|
+
CLAUDE_OPUS_5_MODEL,
|
|
1800
|
+
CLAUDE_OPUS_4_8_MODEL,
|
|
1801
|
+
GPT_5_6_SOL_MODEL,
|
|
1802
|
+
GPT_5_6_TERRA_MODEL,
|
|
1803
|
+
GPT_5_6_LUNA_MODEL,
|
|
1804
|
+
"gpt-5.5",
|
|
1805
|
+
"composer-2.5",
|
|
1806
|
+
CLAUDE_SONNET_5_MODEL,
|
|
1807
|
+
"claude-opus-4-7",
|
|
1808
|
+
"gpt-5.4",
|
|
1809
|
+
"gpt-5.4-mini",
|
|
1810
|
+
"gpt-5.4-nano",
|
|
1811
|
+
"gpt-5.3-codex",
|
|
1812
|
+
"gpt-5.2",
|
|
1813
|
+
"gpt-5.2-codex",
|
|
1814
|
+
"gpt-5.1",
|
|
1815
|
+
"gpt-5.1-codex-max",
|
|
1816
|
+
"gpt-5.1-codex-mini",
|
|
1817
|
+
"gpt-5-mini",
|
|
1818
|
+
"gemini-3.1-pro",
|
|
1819
|
+
"gemini-3-flash",
|
|
1820
|
+
"grok-4.6",
|
|
1821
|
+
"grok-4.5",
|
|
1822
|
+
"grok-4.3",
|
|
1823
|
+
CLAUDE_HAIKU_4_5_MODEL,
|
|
1824
|
+
"kimi-k2.5"
|
|
1825
|
+
],
|
|
1826
|
+
deepseek: prioritizeDeepseekModels(OPENROUTER_MODELS),
|
|
1827
|
+
fx: [DEFAULT_FX_MODEL],
|
|
1828
|
+
opencode: [...OPENROUTER_MODELS, ...ASTER_MODELS],
|
|
1829
|
+
pi: [...OPENROUTER_MODELS, ...ASTER_MODELS],
|
|
1830
|
+
relay: [CLAUDE_FABLE_5_MODEL, CLAUDE_OPUS_5_MODEL, CLAUDE_OPUS_4_8_MODEL, CLAUDE_SONNET_5_MODEL]
|
|
1831
|
+
};
|
|
1832
|
+
var MODEL_LABELS = {
|
|
1833
|
+
fable: "Fable 5",
|
|
1834
|
+
[CLAUDE_FABLE_5_MODEL]: "Fable 5",
|
|
1835
|
+
sonnet: "Sonnet 5",
|
|
1836
|
+
"claude-sonnet-5": "Sonnet 5",
|
|
1837
|
+
opus: "Opus 5",
|
|
1838
|
+
[CLAUDE_OPUS_5_MODEL]: "Opus 5",
|
|
1839
|
+
[CLAUDE_OPUS_1M_MODEL]: "Opus (1M)",
|
|
1840
|
+
[LEGACY_CLAUDE_OPUS_1M_MODEL]: "Opus (1M)",
|
|
1841
|
+
haiku: "Haiku 4.5",
|
|
1842
|
+
[GPT_5_6_SOL_MODEL]: "GPT-5.6 Sol",
|
|
1843
|
+
[GPT_5_6_TERRA_MODEL]: "GPT-5.6 Terra",
|
|
1844
|
+
[GPT_5_6_LUNA_MODEL]: "GPT-5.6 Luna",
|
|
1845
|
+
"gpt-5.5": "GPT-5.5",
|
|
1846
|
+
[DEFAULT_OPENCODE_MODEL]: "GLM 5.2 via OpenRouter",
|
|
1847
|
+
[DEEPSEEK_V4_PRO_MODEL]: "DeepSeek V4 Pro via OpenRouter",
|
|
1848
|
+
[DEEPSEEK_V4_FLASH_MODEL]: "DeepSeek V4 Flash via OpenRouter",
|
|
1849
|
+
"minimax/minimax-m3": "MiniMax M3 via OpenRouter",
|
|
1850
|
+
"xiaomi/mimo-v2.5-pro": "MiMo V2.5 Pro via OpenRouter",
|
|
1851
|
+
"moonshotai/kimi-k2.6": "Kimi K2.6 via OpenRouter",
|
|
1852
|
+
...ASTER_MODEL_LABELS,
|
|
1853
|
+
"gpt-5.4": "GPT-5.4",
|
|
1854
|
+
"gpt-5.4-mini": "GPT-5.4 Mini",
|
|
1855
|
+
"gpt-5.4-nano": "GPT-5.4 Nano",
|
|
1856
|
+
"gpt-5.3-codex": "GPT-5.3 Codex",
|
|
1857
|
+
"gpt-5.2": "GPT-5.2",
|
|
1858
|
+
"gpt-5.2-codex": "GPT-5.2 Codex",
|
|
1859
|
+
"gpt-5.1": "GPT-5.1",
|
|
1860
|
+
"gpt-5.1-codex-max": "GPT-5.1 Codex Max",
|
|
1861
|
+
"gpt-5.1-codex-mini": "GPT-5.1 Codex Mini",
|
|
1862
|
+
"gpt-5-mini": "GPT-5 Mini",
|
|
1863
|
+
"composer-2.5": "Composer 2.5",
|
|
1864
|
+
[CLAUDE_OPUS_4_8_MODEL]: "Opus 4.8",
|
|
1865
|
+
"claude-opus-4-7": "Opus 4.7",
|
|
1866
|
+
[CLAUDE_HAIKU_4_5_MODEL]: "Haiku 4.5",
|
|
1867
|
+
"gemini-3.1-pro": "Gemini 3.1 Pro",
|
|
1868
|
+
"gemini-3-flash": "Gemini 3 Flash",
|
|
1869
|
+
"grok-4.6": "Grok 4.6",
|
|
1870
|
+
"grok-4.5": "Grok 4.5",
|
|
1871
|
+
"grok-4.3": "Grok 4.3",
|
|
1872
|
+
"kimi-k2.5": "Kimi K2.5"
|
|
1873
|
+
};
|
|
1874
|
+
var IMAGE_MEDIA_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"];
|
|
1875
|
+
var MAX_CANVAS_FILE_BYTES = 25 * 1024 * 1024;
|
|
1876
|
+
var MAX_CANVAS_PREVIEW_BYTES = 5 * 1024 * 1024;
|
|
1877
|
+
|
|
1878
|
+
// ../shared/src/engine/v1.ts
|
|
1879
|
+
import { z as z2 } from "zod";
|
|
1880
|
+
var createChatRequestSchema = z2.object({
|
|
1881
|
+
id: z2.string().uuid().optional(),
|
|
1882
|
+
createdAt: z2.string().datetime().optional(),
|
|
1883
|
+
provider: z2.enum(VALID_AGENT_PROVIDERS),
|
|
1884
|
+
title: z2.string().min(1).optional(),
|
|
1885
|
+
parentChatId: z2.string().uuid().optional(),
|
|
1886
|
+
clientRequestId: z2.string().min(1).max(128).optional()
|
|
1887
|
+
});
|
|
1888
|
+
var sendChatMessageRequestSchema = z2.object({
|
|
1889
|
+
messageId: z2.string().optional(),
|
|
1890
|
+
submittedAt: z2.string().optional(),
|
|
1891
|
+
message: z2.string(),
|
|
1892
|
+
model: z2.string().optional(),
|
|
1893
|
+
customInstructions: z2.string().optional(),
|
|
1894
|
+
planMode: z2.boolean().optional(),
|
|
1895
|
+
images: z2.array(z2.object({
|
|
1896
|
+
type: z2.literal("image"),
|
|
1897
|
+
source: z2.discriminatedUnion("type", [
|
|
1898
|
+
z2.object({ type: z2.literal("base64"), media_type: z2.enum(IMAGE_MEDIA_TYPES), data: z2.string() }),
|
|
1899
|
+
z2.object({ type: z2.literal("url"), url: z2.string() })
|
|
1900
|
+
])
|
|
1901
|
+
})).optional(),
|
|
1902
|
+
thinkingLevel: z2.enum(VALID_THINKING_LEVELS).optional(),
|
|
1903
|
+
goalMode: z2.boolean().optional(),
|
|
1904
|
+
fastMode: z2.boolean().optional(),
|
|
1905
|
+
enableInteractiveTools: z2.boolean().optional(),
|
|
1906
|
+
type: z2.string().optional(),
|
|
1907
|
+
merge: z2.boolean().optional(),
|
|
1908
|
+
idempotencyKey: z2.string().optional(),
|
|
1909
|
+
senderUserId: z2.string().optional(),
|
|
1910
|
+
senderEmail: z2.string().optional(),
|
|
1911
|
+
senderDisplayName: z2.string().optional(),
|
|
1912
|
+
senderAvatarUrl: z2.string().optional(),
|
|
1913
|
+
errorNotificationTarget: z2.discriminatedUnion("type", [
|
|
1914
|
+
z2.object({ type: z2.literal("slack") }),
|
|
1915
|
+
z2.object({ type: z2.literal("linear"), sessionId: z2.string() }),
|
|
1916
|
+
z2.object({
|
|
1917
|
+
type: z2.literal("code_host"),
|
|
1918
|
+
provider: z2.enum(["github", "gitlab"]),
|
|
1919
|
+
resource: z2.enum(["issue", "pull_request"]),
|
|
1920
|
+
repositoryId: z2.string(),
|
|
1921
|
+
resourceNumber: z2.number()
|
|
1922
|
+
}),
|
|
1923
|
+
z2.object({ type: z2.literal("automation"), executionId: z2.string() })
|
|
1924
|
+
]).optional()
|
|
1925
|
+
}).passthrough();
|
|
1926
|
+
|
|
1927
|
+
// ../shared/src/display-message/types.ts
|
|
1928
|
+
var BACKGROUND_TASK_SUBTYPES = /* @__PURE__ */ new Set([
|
|
1929
|
+
"task_started",
|
|
1930
|
+
"task_progress",
|
|
1931
|
+
"task_updated",
|
|
1932
|
+
"task_notification"
|
|
1933
|
+
]);
|
|
1934
|
+
function isBackgroundTaskSubtype(subtype) {
|
|
1935
|
+
return typeof subtype === "string" && BACKGROUND_TASK_SUBTYPES.has(subtype);
|
|
1936
|
+
}
|
|
1937
|
+
function optionalString(value) {
|
|
1938
|
+
return typeof value === "string" ? value : void 0;
|
|
1939
|
+
}
|
|
1940
|
+
function coerceBackgroundTaskPayload(payload) {
|
|
1941
|
+
if (!isRecord(payload)) return null;
|
|
1942
|
+
const subtype = payload.subtype;
|
|
1943
|
+
if (!isBackgroundTaskSubtype(subtype)) return null;
|
|
1944
|
+
const taskId = payload.task_id;
|
|
1945
|
+
if (typeof taskId !== "string" || !taskId) return null;
|
|
1946
|
+
const patch = isRecord(payload.patch) ? {
|
|
1947
|
+
status: optionalString(payload.patch.status),
|
|
1948
|
+
description: optionalString(payload.patch.description),
|
|
1949
|
+
error: optionalString(payload.patch.error),
|
|
1950
|
+
isBackgrounded: optionalBoolean(payload.patch.is_backgrounded)
|
|
1951
|
+
} : void 0;
|
|
1952
|
+
const usage = isRecord(payload.usage) ? {
|
|
1953
|
+
totalTokens: optionalNumber(payload.usage.total_tokens),
|
|
1954
|
+
toolUses: optionalNumber(payload.usage.tool_uses),
|
|
1955
|
+
durationMs: optionalNumber(payload.usage.duration_ms)
|
|
1956
|
+
} : void 0;
|
|
1596
1957
|
return {
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1958
|
+
subtype,
|
|
1959
|
+
taskId,
|
|
1960
|
+
description: optionalString(payload.description),
|
|
1961
|
+
summary: optionalString(payload.summary),
|
|
1962
|
+
outputFile: optionalString(payload.output_file),
|
|
1963
|
+
status: optionalString(payload.status),
|
|
1964
|
+
taskType: optionalString(payload.task_type),
|
|
1965
|
+
workflowName: optionalString(payload.workflow_name),
|
|
1966
|
+
prompt: optionalString(payload.prompt),
|
|
1967
|
+
lastToolName: optionalString(payload.last_tool_name),
|
|
1968
|
+
usage,
|
|
1969
|
+
patch
|
|
1602
1970
|
};
|
|
1603
1971
|
}
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1972
|
+
function optionalNumber(value) {
|
|
1973
|
+
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
1974
|
+
}
|
|
1975
|
+
function optionalBoolean(value) {
|
|
1976
|
+
return typeof value === "boolean" ? value : void 0;
|
|
1977
|
+
}
|
|
1978
|
+
function normalizeBackgroundTaskStatus(status) {
|
|
1979
|
+
if (status === "completed" || status === "failed" || status === "stopped") {
|
|
1980
|
+
return status;
|
|
1981
|
+
}
|
|
1982
|
+
if (status === "killed") {
|
|
1983
|
+
return "stopped";
|
|
1984
|
+
}
|
|
1985
|
+
return "in_progress";
|
|
1986
|
+
}
|
|
1987
|
+
|
|
1988
|
+
// ../shared/src/display-message/constants.ts
|
|
1989
|
+
var USER_MESSAGE_MATCH_GRACE_PERIOD_MS = 3e4;
|
|
1607
1990
|
|
|
1608
1991
|
// ../shared/src/json.ts
|
|
1609
1992
|
function safeJsonParse(str, fallback) {
|
|
@@ -1614,9 +1997,286 @@ function safeJsonParse(str, fallback) {
|
|
|
1614
1997
|
}
|
|
1615
1998
|
}
|
|
1616
1999
|
|
|
1617
|
-
// ../shared/src/
|
|
1618
|
-
function
|
|
1619
|
-
|
|
2000
|
+
// ../shared/src/display-message/parsers/utils.ts
|
|
2001
|
+
function userMessageImages(value) {
|
|
2002
|
+
if (!Array.isArray(value)) return void 0;
|
|
2003
|
+
const images = value.filter((item) => isRecord(item) && item.type === "image" && typeof item.mediaType === "string" && typeof item.data === "string");
|
|
2004
|
+
return images.length > 0 ? images : void 0;
|
|
2005
|
+
}
|
|
2006
|
+
function stringifyDisplayValue(value) {
|
|
2007
|
+
if (value === void 0 || value === null) return void 0;
|
|
2008
|
+
if (typeof value === "string") return value;
|
|
2009
|
+
try {
|
|
2010
|
+
return JSON.stringify(value, null, 2);
|
|
2011
|
+
} catch {
|
|
2012
|
+
return String(value);
|
|
2013
|
+
}
|
|
2014
|
+
}
|
|
2015
|
+
function upsertDisplayMessage(messages, message) {
|
|
2016
|
+
const index = messages.findIndex((candidate) => candidate.id === message.id);
|
|
2017
|
+
if (index === -1) messages.push(message);
|
|
2018
|
+
else messages[index] = message;
|
|
2019
|
+
}
|
|
2020
|
+
function parseSkillName(tool, input) {
|
|
2021
|
+
if (typeof tool !== "string") return null;
|
|
2022
|
+
const normalizedTool = tool.toLowerCase();
|
|
2023
|
+
if (normalizedTool.startsWith("skill.")) return tool.slice("skill.".length) || null;
|
|
2024
|
+
if (normalizedTool !== "skill") return null;
|
|
2025
|
+
const parsedInput = typeof input === "string" ? safeJsonParse(input, null) : input;
|
|
2026
|
+
if (!isRecord(parsedInput)) return null;
|
|
2027
|
+
const skillName = parsedInput.skill ?? parsedInput.name;
|
|
2028
|
+
return typeof skillName === "string" && skillName ? skillName : null;
|
|
2029
|
+
}
|
|
2030
|
+
function skillNameFromFilePath(path) {
|
|
2031
|
+
const skillName = skillNameFromSkillRootPath(path);
|
|
2032
|
+
if (skillName) return skillName;
|
|
2033
|
+
const parts = path.split(/[\\/]+/).filter(Boolean);
|
|
2034
|
+
if (parts.length < 2 || parts[parts.length - 1].toLowerCase() !== "skill.md") return null;
|
|
2035
|
+
const fallbackSkillName = parts[parts.length - 2];
|
|
2036
|
+
return fallbackSkillName && fallbackSkillName.toLowerCase() !== "skills" ? fallbackSkillName : null;
|
|
2037
|
+
}
|
|
2038
|
+
function skillNameFromSkillRootPath(path) {
|
|
2039
|
+
const parts = path.split(/[\\/]+/).filter(Boolean);
|
|
2040
|
+
const skillsIndex = parts.findLastIndex((part) => part.toLowerCase() === "skills");
|
|
2041
|
+
if (skillsIndex === -1) return null;
|
|
2042
|
+
const firstSkillPart = parts[skillsIndex + 1];
|
|
2043
|
+
const skillName = firstSkillPart?.toLowerCase() === ".system" ? parts[skillsIndex + 2] : firstSkillPart;
|
|
2044
|
+
return skillName && skillName.toLowerCase() !== "skill.md" ? skillName : null;
|
|
2045
|
+
}
|
|
2046
|
+
function parseSkillFilePath(input) {
|
|
2047
|
+
const parsedInput = typeof input === "string" ? safeJsonParse(input, null) : input;
|
|
2048
|
+
if (!isRecord(parsedInput)) return null;
|
|
2049
|
+
const path = [parsedInput.path, parsedInput.filePath, parsedInput.file_path].find((value) => typeof value === "string" && value.length > 0);
|
|
2050
|
+
if (!path) return null;
|
|
2051
|
+
return skillNameFromFilePath(path);
|
|
2052
|
+
}
|
|
2053
|
+
function skillNameFromToolCall(tool, input) {
|
|
2054
|
+
const directSkillName = parseSkillName(tool, input);
|
|
2055
|
+
if (directSkillName) return directSkillName;
|
|
2056
|
+
if (typeof tool !== "string" || !["read", "read_file", "readfile"].includes(tool.toLowerCase())) return null;
|
|
2057
|
+
return parseSkillFilePath(input);
|
|
2058
|
+
}
|
|
2059
|
+
function createCallDisplayMessage(message) {
|
|
2060
|
+
const skillName = message.server.toLowerCase() === "skills" ? message.tool : skillNameFromToolCall(message.tool, message.input);
|
|
2061
|
+
if (skillName === null) return { ...message, type: "tool_call" };
|
|
2062
|
+
return {
|
|
2063
|
+
id: message.id,
|
|
2064
|
+
type: "skill",
|
|
2065
|
+
skillName,
|
|
2066
|
+
status: message.status,
|
|
2067
|
+
timestamp: message.timestamp
|
|
2068
|
+
};
|
|
2069
|
+
}
|
|
2070
|
+
|
|
2071
|
+
// ../shared/src/display-message/format.ts
|
|
2072
|
+
function unquoteShellArg(value) {
|
|
2073
|
+
const trimmed = value.trim();
|
|
2074
|
+
if (trimmed.length < 2) return trimmed;
|
|
2075
|
+
if (trimmed[0] === "'" && trimmed[trimmed.length - 1] === "'") {
|
|
2076
|
+
return trimmed.slice(1, -1).replace(/'\\''/g, "'");
|
|
2077
|
+
}
|
|
2078
|
+
if (trimmed[0] === '"' && trimmed[trimmed.length - 1] === '"') {
|
|
2079
|
+
return trimmed.slice(1, -1).replace(/\\(["\\$`])/g, "$1");
|
|
2080
|
+
}
|
|
2081
|
+
return trimmed;
|
|
2082
|
+
}
|
|
2083
|
+
function shellPayload(command) {
|
|
2084
|
+
const match = command.trim().match(/^(?:\S*\/)?(?:ba|z|c|fi)?sh\s+-(?:[a-zA-Z]*c[a-zA-Z]*|c)\s+([\s\S]+)$/);
|
|
2085
|
+
return match ? unquoteShellArg(match[1]) : command.trim();
|
|
2086
|
+
}
|
|
2087
|
+
function splitShellWords(command) {
|
|
2088
|
+
const words = [];
|
|
2089
|
+
let word = "";
|
|
2090
|
+
let quote = null;
|
|
2091
|
+
let escaped = false;
|
|
2092
|
+
for (const char of command) {
|
|
2093
|
+
if (escaped) {
|
|
2094
|
+
word += char;
|
|
2095
|
+
escaped = false;
|
|
2096
|
+
continue;
|
|
2097
|
+
}
|
|
2098
|
+
if (char === "\\" && quote !== "'") {
|
|
2099
|
+
escaped = true;
|
|
2100
|
+
continue;
|
|
2101
|
+
}
|
|
2102
|
+
if (quote) {
|
|
2103
|
+
if (char === quote) quote = null;
|
|
2104
|
+
else word += char;
|
|
2105
|
+
continue;
|
|
2106
|
+
}
|
|
2107
|
+
if (char === '"' || char === "'") {
|
|
2108
|
+
quote = char;
|
|
2109
|
+
continue;
|
|
2110
|
+
}
|
|
2111
|
+
if (/\s/.test(char)) {
|
|
2112
|
+
if (word) {
|
|
2113
|
+
words.push(word);
|
|
2114
|
+
word = "";
|
|
2115
|
+
}
|
|
2116
|
+
continue;
|
|
2117
|
+
}
|
|
2118
|
+
word += char;
|
|
2119
|
+
}
|
|
2120
|
+
if (word) words.push(word);
|
|
2121
|
+
return words;
|
|
2122
|
+
}
|
|
2123
|
+
function commandWords(command) {
|
|
2124
|
+
const payload = shellPayload(command);
|
|
2125
|
+
if (!payload || /[;|<>]|&&/.test(payload)) return null;
|
|
2126
|
+
return splitShellWords(payload);
|
|
2127
|
+
}
|
|
2128
|
+
function skillNamesFromCommand(command) {
|
|
2129
|
+
const words = commandWords(command);
|
|
2130
|
+
if (!words) return [];
|
|
2131
|
+
return Array.from(new Set(words.flatMap((word) => {
|
|
2132
|
+
const skillName = skillNameFromSkillRootPath(word);
|
|
2133
|
+
return skillName ? [skillName] : [];
|
|
2134
|
+
})));
|
|
2135
|
+
}
|
|
2136
|
+
|
|
2137
|
+
// ../shared/src/display-message/task-accumulator.ts
|
|
2138
|
+
function mapTaskStatus(status) {
|
|
2139
|
+
if (status === "in_progress" || status === "completed") return status;
|
|
2140
|
+
return "pending";
|
|
2141
|
+
}
|
|
2142
|
+
function toAccumulatorInput(input) {
|
|
2143
|
+
if (typeof input === "string") {
|
|
2144
|
+
try {
|
|
2145
|
+
const parsed = JSON.parse(input);
|
|
2146
|
+
return isRecord(parsed) ? parsed : {};
|
|
2147
|
+
} catch {
|
|
2148
|
+
return {};
|
|
2149
|
+
}
|
|
2150
|
+
}
|
|
2151
|
+
return isRecord(input) ? input : {};
|
|
2152
|
+
}
|
|
2153
|
+
function extractToolResultText(content, separator = "\n") {
|
|
2154
|
+
if (typeof content === "string") return content;
|
|
2155
|
+
if (!Array.isArray(content)) return "";
|
|
2156
|
+
const texts = [];
|
|
2157
|
+
for (const part of content) {
|
|
2158
|
+
if (isRecord(part) && part.type === "text" && typeof part.text === "string") {
|
|
2159
|
+
texts.push(part.text);
|
|
2160
|
+
}
|
|
2161
|
+
}
|
|
2162
|
+
return texts.join(separator);
|
|
2163
|
+
}
|
|
2164
|
+
function normalizeContentBlocks(content) {
|
|
2165
|
+
if (typeof content === "string") {
|
|
2166
|
+
return content ? [{ type: "text", text: content }] : [];
|
|
2167
|
+
}
|
|
2168
|
+
return Array.isArray(content) ? content.filter((block) => isRecord(block) && typeof block.type === "string") : [];
|
|
2169
|
+
}
|
|
2170
|
+
function parseStringField(value) {
|
|
2171
|
+
if (typeof value !== "string") return null;
|
|
2172
|
+
const trimmed = value.trim();
|
|
2173
|
+
return trimmed ? trimmed : null;
|
|
2174
|
+
}
|
|
2175
|
+
function parseCreateResultTaskId(resultText) {
|
|
2176
|
+
try {
|
|
2177
|
+
const parsed = JSON.parse(resultText);
|
|
2178
|
+
if (isRecord(parsed) && isRecord(parsed.task)) {
|
|
2179
|
+
return parseStringField(parsed.task.id);
|
|
2180
|
+
}
|
|
2181
|
+
} catch {
|
|
2182
|
+
}
|
|
2183
|
+
return resultText.match(/\bTask\s+#?([A-Za-z0-9_-]+)\s+created\b/i)?.[1] ?? null;
|
|
2184
|
+
}
|
|
2185
|
+
var TaskAccumulator = class {
|
|
2186
|
+
tasksByTaskId = /* @__PURE__ */ new Map();
|
|
2187
|
+
tasksByCreateToolUseId = /* @__PURE__ */ new Map();
|
|
2188
|
+
orderedKeys = [];
|
|
2189
|
+
processToolUse(toolName, toolUseId, input) {
|
|
2190
|
+
if (toolName === "TaskCreate") {
|
|
2191
|
+
const subject = parseStringField(input.subject);
|
|
2192
|
+
if (!subject) return false;
|
|
2193
|
+
this.tasksByCreateToolUseId.set(toolUseId, { subject, status: "pending" });
|
|
2194
|
+
this.orderedKeys.push(toolUseId);
|
|
2195
|
+
return true;
|
|
2196
|
+
}
|
|
2197
|
+
if (toolName === "TaskUpdate") {
|
|
2198
|
+
const taskId = parseStringField(input.taskId);
|
|
2199
|
+
if (!taskId) return false;
|
|
2200
|
+
if (input.status === "deleted") {
|
|
2201
|
+
if (!this.tasksByTaskId.delete(taskId)) return false;
|
|
2202
|
+
const idx = this.orderedKeys.indexOf(taskId);
|
|
2203
|
+
if (idx !== -1) this.orderedKeys.splice(idx, 1);
|
|
2204
|
+
return true;
|
|
2205
|
+
}
|
|
2206
|
+
const existing = this.tasksByTaskId.get(taskId);
|
|
2207
|
+
const subject = parseStringField(input.subject) ?? existing?.subject;
|
|
2208
|
+
if (!subject) return false;
|
|
2209
|
+
const status = input.status !== void 0 ? mapTaskStatus(input.status) : existing?.status ?? "pending";
|
|
2210
|
+
this.tasksByTaskId.set(taskId, { subject, status });
|
|
2211
|
+
if (!existing) this.orderedKeys.push(taskId);
|
|
2212
|
+
return true;
|
|
2213
|
+
}
|
|
2214
|
+
if (toolName === "TaskList") {
|
|
2215
|
+
if (!Array.isArray(input.tasks)) return false;
|
|
2216
|
+
let changed = false;
|
|
2217
|
+
for (const raw of input.tasks) {
|
|
2218
|
+
if (!isRecord(raw)) continue;
|
|
2219
|
+
const id = parseStringField(raw.id);
|
|
2220
|
+
const subject = parseStringField(raw.subject);
|
|
2221
|
+
if (!id || !subject) continue;
|
|
2222
|
+
const existing = this.tasksByTaskId.get(id);
|
|
2223
|
+
const next = { subject, status: mapTaskStatus(raw.status) };
|
|
2224
|
+
if (existing && existing.subject === next.subject && existing.status === next.status) continue;
|
|
2225
|
+
this.tasksByTaskId.set(id, next);
|
|
2226
|
+
if (!existing) this.orderedKeys.push(id);
|
|
2227
|
+
changed = true;
|
|
2228
|
+
}
|
|
2229
|
+
return changed;
|
|
2230
|
+
}
|
|
2231
|
+
return false;
|
|
2232
|
+
}
|
|
2233
|
+
processCreateResult(toolUseId, resultText) {
|
|
2234
|
+
const pending = this.tasksByCreateToolUseId.get(toolUseId);
|
|
2235
|
+
if (!pending) return false;
|
|
2236
|
+
const taskId = parseCreateResultTaskId(resultText);
|
|
2237
|
+
if (!taskId) return false;
|
|
2238
|
+
this.tasksByTaskId.set(taskId, pending);
|
|
2239
|
+
this.tasksByCreateToolUseId.delete(toolUseId);
|
|
2240
|
+
const idx = this.orderedKeys.indexOf(toolUseId);
|
|
2241
|
+
if (idx !== -1) this.orderedKeys.splice(idx, 1, taskId);
|
|
2242
|
+
return true;
|
|
2243
|
+
}
|
|
2244
|
+
getTasks() {
|
|
2245
|
+
const out = [];
|
|
2246
|
+
for (const key of this.orderedKeys) {
|
|
2247
|
+
const task = this.tasksByTaskId.get(key) ?? this.tasksByCreateToolUseId.get(key);
|
|
2248
|
+
if (task && task.subject) out.push(task);
|
|
2249
|
+
}
|
|
2250
|
+
return out;
|
|
2251
|
+
}
|
|
2252
|
+
processContentBlock(block) {
|
|
2253
|
+
if (!isRecord(block)) return false;
|
|
2254
|
+
if (block.type === "tool_use" && typeof block.name === "string" && typeof block.id === "string") {
|
|
2255
|
+
return this.processToolUse(block.name, block.id, toAccumulatorInput(block.input));
|
|
2256
|
+
}
|
|
2257
|
+
if (block.type === "tool_result" && typeof block.tool_use_id === "string") {
|
|
2258
|
+
return this.processCreateResult(block.tool_use_id, extractToolResultText(block.content, ""));
|
|
2259
|
+
}
|
|
2260
|
+
return false;
|
|
2261
|
+
}
|
|
2262
|
+
};
|
|
2263
|
+
|
|
2264
|
+
// ../shared/src/user-message-matching.ts
|
|
2265
|
+
function parseTimestampMs(timestamp) {
|
|
2266
|
+
const value = Date.parse(timestamp);
|
|
2267
|
+
return Number.isFinite(value) ? value : 0;
|
|
2268
|
+
}
|
|
2269
|
+
function areUserMessagesWithinMatchWindow(a, b) {
|
|
2270
|
+
return a.content === b.content && Math.abs(parseTimestampMs(a.timestamp) - parseTimestampMs(b.timestamp)) <= USER_MESSAGE_MATCH_GRACE_PERIOD_MS;
|
|
2271
|
+
}
|
|
2272
|
+
|
|
2273
|
+
// ../shared/src/agent-event-utils.ts
|
|
2274
|
+
function getUserMessage(event) {
|
|
2275
|
+
return event.type === "event_msg" && event.payload.type === "user_message" && typeof event.payload.message === "string" ? event.payload.message : null;
|
|
2276
|
+
}
|
|
2277
|
+
function getUserMessageId(event) {
|
|
2278
|
+
const messageId = event.payload[USER_MESSAGE_ID_PAYLOAD_KEY];
|
|
2279
|
+
return typeof messageId === "string" ? messageId : null;
|
|
1620
2280
|
}
|
|
1621
2281
|
|
|
1622
2282
|
// ../shared/src/display-message/parsers/codex-parser.ts
|
|
@@ -1631,14 +2291,6 @@ function displayId(event, eventIndex, prefix) {
|
|
|
1631
2291
|
const stableId = getPayloadString(event, USER_MESSAGE_ID_PAYLOAD_KEY) ?? getPayloadString(event, CODEX_ASP_ITEM_ID_PAYLOAD_KEY);
|
|
1632
2292
|
return stableId ? `${prefix}-${stableId}` : `${prefix}-${event.timestamp}-${eventIndex}`;
|
|
1633
2293
|
}
|
|
1634
|
-
function userMessageImages(value) {
|
|
1635
|
-
if (!Array.isArray(value)) return void 0;
|
|
1636
|
-
const images = value.filter((item) => {
|
|
1637
|
-
if (!isRecord(item)) return false;
|
|
1638
|
-
return item.type === "image" && typeof item.mediaType === "string" && typeof item.data === "string";
|
|
1639
|
-
});
|
|
1640
|
-
return images.length > 0 ? images : void 0;
|
|
1641
|
-
}
|
|
1642
2294
|
function parseShellOutput(raw) {
|
|
1643
2295
|
const exitCodeMatch = raw.match(/^Exit code: (\d+)/m) || raw.match(/Process exited with code (\d+)/);
|
|
1644
2296
|
const exitCode = exitCodeMatch ? parseInt(exitCodeMatch[1], 10) : 0;
|
|
@@ -1758,10 +2410,12 @@ function parseCodexEvents(events) {
|
|
|
1758
2410
|
const callId = event.payload.call_id;
|
|
1759
2411
|
const args = safeJsonParse(event.payload.arguments || "{}", {});
|
|
1760
2412
|
const command = args.cmd || (Array.isArray(args.command) ? args.command.join(" ") : args.command) || "";
|
|
2413
|
+
const skillNames = skillNamesFromCommand(command);
|
|
1761
2414
|
const msg = {
|
|
1762
2415
|
id: `command-${callId || "no-call-id"}-${eventIndex}`,
|
|
1763
2416
|
type: "command",
|
|
1764
2417
|
command,
|
|
2418
|
+
...skillNames.length > 0 ? { skillNames } : {},
|
|
1765
2419
|
output: "",
|
|
1766
2420
|
status: "in_progress",
|
|
1767
2421
|
timestamp: event.timestamp
|
|
@@ -1793,17 +2447,17 @@ function parseCodexEvents(events) {
|
|
|
1793
2447
|
const operations = parsePatch(input);
|
|
1794
2448
|
pendingPatches.set(callId, { input, status, timestamp: event.timestamp, operations });
|
|
1795
2449
|
} else {
|
|
1796
|
-
const
|
|
1797
|
-
|
|
1798
|
-
|
|
2450
|
+
const id = `toolcall-${callId || getPayloadString(event, CODEX_ASP_ITEM_ID_PAYLOAD_KEY) || `${event.timestamp}-${eventIndex}`}`;
|
|
2451
|
+
const msg = createCallDisplayMessage({
|
|
2452
|
+
id,
|
|
1799
2453
|
server: server2,
|
|
1800
2454
|
tool: name,
|
|
1801
2455
|
input,
|
|
1802
2456
|
status,
|
|
1803
2457
|
timestamp: event.timestamp
|
|
1804
|
-
};
|
|
2458
|
+
});
|
|
1805
2459
|
messages.push(msg);
|
|
1806
|
-
if (callId) {
|
|
2460
|
+
if (callId && msg.type === "tool_call") {
|
|
1807
2461
|
pendingToolCalls.set(callId, msg);
|
|
1808
2462
|
}
|
|
1809
2463
|
}
|
|
@@ -1862,17 +2516,6 @@ function parseCodexEvents(events) {
|
|
|
1862
2516
|
return messages;
|
|
1863
2517
|
}
|
|
1864
2518
|
|
|
1865
|
-
// ../shared/src/display-message/parsers/utils.ts
|
|
1866
|
-
function stringifyDisplayValue(value) {
|
|
1867
|
-
if (value === void 0 || value === null) return void 0;
|
|
1868
|
-
if (typeof value === "string") return value;
|
|
1869
|
-
try {
|
|
1870
|
-
return JSON.stringify(value, null, 2);
|
|
1871
|
-
} catch {
|
|
1872
|
-
return String(value);
|
|
1873
|
-
}
|
|
1874
|
-
}
|
|
1875
|
-
|
|
1876
2519
|
// ../shared/src/display-message/parsers/cursor-parser.ts
|
|
1877
2520
|
function getTextContent(value) {
|
|
1878
2521
|
if (!Array.isArray(value)) return "";
|
|
@@ -1898,10 +2541,10 @@ function getCursorThinkingText(event) {
|
|
|
1898
2541
|
function isTerminalStatus(status) {
|
|
1899
2542
|
return status === "FINISHED" || status === "ERROR" || status === "CANCELLED" || status === "EXPIRED";
|
|
1900
2543
|
}
|
|
1901
|
-
function
|
|
1902
|
-
for (const index of
|
|
2544
|
+
function finalizeOpenCalls(messages, callIndexes, status) {
|
|
2545
|
+
for (const index of callIndexes.values()) {
|
|
1903
2546
|
const message = messages[index];
|
|
1904
|
-
if (message?.type === "tool_call" && message.status === "in_progress") {
|
|
2547
|
+
if ((message?.type === "tool_call" || message?.type === "skill") && message.status === "in_progress") {
|
|
1905
2548
|
messages[index] = {
|
|
1906
2549
|
...message,
|
|
1907
2550
|
status
|
|
@@ -1911,7 +2554,7 @@ function finalizeOpenTools(messages, toolIndexes, status) {
|
|
|
1911
2554
|
}
|
|
1912
2555
|
function parseCursorEvents(events) {
|
|
1913
2556
|
const messages = [];
|
|
1914
|
-
const
|
|
2557
|
+
const callIndexes = /* @__PURE__ */ new Map();
|
|
1915
2558
|
const runAssistantSegments = /* @__PURE__ */ new Map();
|
|
1916
2559
|
const runThinkingSegments = /* @__PURE__ */ new Map();
|
|
1917
2560
|
let activeAssistant = null;
|
|
@@ -2003,21 +2646,20 @@ function parseCursorEvents(events) {
|
|
|
2003
2646
|
const tool = typeof event.payload.name === "string" ? event.payload.name : "tool";
|
|
2004
2647
|
const status = cursorStatusToDisplayStatus(event.payload.status);
|
|
2005
2648
|
const input = isRecord(event.payload.args) ? event.payload.args : typeof event.payload.args === "string" ? event.payload.args : void 0;
|
|
2006
|
-
const existing =
|
|
2007
|
-
const next = {
|
|
2649
|
+
const existing = callIndexes.get(callId);
|
|
2650
|
+
const next = createCallDisplayMessage({
|
|
2008
2651
|
id: callId,
|
|
2009
|
-
type: "tool_call",
|
|
2010
2652
|
server: "cursor",
|
|
2011
2653
|
tool,
|
|
2012
2654
|
...input !== void 0 ? { input } : {},
|
|
2013
2655
|
output: stringifyDisplayValue(event.payload.result),
|
|
2014
2656
|
status,
|
|
2015
2657
|
timestamp: event.timestamp
|
|
2016
|
-
};
|
|
2658
|
+
});
|
|
2017
2659
|
if (existing === void 0) {
|
|
2018
|
-
|
|
2660
|
+
callIndexes.set(callId, messages.push(next) - 1);
|
|
2019
2661
|
} else {
|
|
2020
|
-
messages[existing] =
|
|
2662
|
+
messages[existing] = next;
|
|
2021
2663
|
}
|
|
2022
2664
|
continue;
|
|
2023
2665
|
}
|
|
@@ -2039,7 +2681,7 @@ function parseCursorEvents(events) {
|
|
|
2039
2681
|
if (isTerminalStatus(status)) {
|
|
2040
2682
|
const displayStatus = cursorStatusToDisplayStatus(status);
|
|
2041
2683
|
finalizeActiveThinking(displayStatus);
|
|
2042
|
-
|
|
2684
|
+
finalizeOpenCalls(messages, callIndexes, displayStatus);
|
|
2043
2685
|
activeThinking = null;
|
|
2044
2686
|
activeAssistant = null;
|
|
2045
2687
|
}
|
|
@@ -2047,7 +2689,7 @@ function parseCursorEvents(events) {
|
|
|
2047
2689
|
}
|
|
2048
2690
|
if (event.type === "cursor-error") {
|
|
2049
2691
|
finalizeActiveThinking("failed");
|
|
2050
|
-
|
|
2692
|
+
finalizeOpenCalls(messages, callIndexes, "failed");
|
|
2051
2693
|
activeAssistant = null;
|
|
2052
2694
|
activeThinking = null;
|
|
2053
2695
|
const message = typeof event.payload.message === "string" ? event.payload.message : "Cursor run failed";
|
|
@@ -2056,7 +2698,231 @@ function parseCursorEvents(events) {
|
|
|
2056
2698
|
type: "error",
|
|
2057
2699
|
message,
|
|
2058
2700
|
timestamp: event.timestamp
|
|
2059
|
-
});
|
|
2701
|
+
});
|
|
2702
|
+
}
|
|
2703
|
+
}
|
|
2704
|
+
return messages;
|
|
2705
|
+
}
|
|
2706
|
+
|
|
2707
|
+
// ../shared/src/display-message/parsers/deepseek-parser.ts
|
|
2708
|
+
function parseArguments(value) {
|
|
2709
|
+
if (typeof value !== "string") return void 0;
|
|
2710
|
+
const parsed = safeJsonParse(value, value);
|
|
2711
|
+
return isRecord(parsed) ? parsed : stringifyDisplayValue(parsed);
|
|
2712
|
+
}
|
|
2713
|
+
function getDeepseekAssistantMessageText(event, blockType) {
|
|
2714
|
+
if (!isRecord(event) || event.type !== "assistant/message" || !isRecord(event.data) || !isRecord(event.data.message) || !Array.isArray(event.data.message.content)) return "";
|
|
2715
|
+
return event.data.message.content.flatMap((block) => isRecord(block) && block.type === blockType && typeof block.text === "string" ? [block.text] : []).join("");
|
|
2716
|
+
}
|
|
2717
|
+
function parseDeepseekEvents(events) {
|
|
2718
|
+
const messages = [];
|
|
2719
|
+
for (const source of events) {
|
|
2720
|
+
if (source.type === "event_msg" && source.payload.type === "user_message") {
|
|
2721
|
+
if (typeof source.payload.message === "string" && source.payload.message.trim()) {
|
|
2722
|
+
const messageId = source.payload[USER_MESSAGE_ID_PAYLOAD_KEY];
|
|
2723
|
+
messages.push({
|
|
2724
|
+
id: typeof messageId === "string" ? messageId : `deepseek-user-${source.timestamp}`,
|
|
2725
|
+
type: "user",
|
|
2726
|
+
content: source.payload.message,
|
|
2727
|
+
timestamp: source.timestamp
|
|
2728
|
+
});
|
|
2729
|
+
}
|
|
2730
|
+
continue;
|
|
2731
|
+
}
|
|
2732
|
+
if (source.type === "deepseek-error") {
|
|
2733
|
+
messages.push({
|
|
2734
|
+
id: `deepseek-error-${source.timestamp}`,
|
|
2735
|
+
type: "error",
|
|
2736
|
+
message: typeof source.payload.message === "string" ? source.payload.message : "DeepSeek Harness failed",
|
|
2737
|
+
timestamp: source.timestamp
|
|
2738
|
+
});
|
|
2739
|
+
continue;
|
|
2740
|
+
}
|
|
2741
|
+
if (source.type === "deepseek-input-request") {
|
|
2742
|
+
const requestId = typeof source.payload.requestId === "string" ? source.payload.requestId : `deepseek-input-${source.timestamp}`;
|
|
2743
|
+
if (source.payload.kind === "approval") {
|
|
2744
|
+
upsertDisplayMessage(messages, createCallDisplayMessage({
|
|
2745
|
+
id: requestId,
|
|
2746
|
+
server: "deepseek",
|
|
2747
|
+
tool: typeof source.payload.toolName === "string" ? source.payload.toolName : "approval",
|
|
2748
|
+
input: typeof source.payload.reason === "string" ? source.payload.reason : void 0,
|
|
2749
|
+
status: "in_progress",
|
|
2750
|
+
timestamp: source.timestamp,
|
|
2751
|
+
inputRequest: {
|
|
2752
|
+
requestId,
|
|
2753
|
+
status: "pending",
|
|
2754
|
+
options: [
|
|
2755
|
+
{ id: "deny", label: "Deny", variant: "secondary" },
|
|
2756
|
+
{ id: "allow", label: "Allow once", variant: "primary" }
|
|
2757
|
+
]
|
|
2758
|
+
}
|
|
2759
|
+
}));
|
|
2760
|
+
} else {
|
|
2761
|
+
const questions = Array.isArray(source.payload.questions) ? source.payload.questions : [];
|
|
2762
|
+
upsertDisplayMessage(messages, createCallDisplayMessage({
|
|
2763
|
+
id: requestId,
|
|
2764
|
+
server: "deepseek",
|
|
2765
|
+
tool: "ask_user",
|
|
2766
|
+
status: "in_progress",
|
|
2767
|
+
timestamp: source.timestamp,
|
|
2768
|
+
inputRequest: {
|
|
2769
|
+
requestId,
|
|
2770
|
+
status: "pending",
|
|
2771
|
+
questions: questions.flatMap((question) => {
|
|
2772
|
+
if (!isRecord(question) || typeof question.id !== "string" || typeof question.question !== "string") return [];
|
|
2773
|
+
const options = Array.isArray(question.options) ? question.options : [];
|
|
2774
|
+
return [{
|
|
2775
|
+
id: question.id,
|
|
2776
|
+
prompt: question.question,
|
|
2777
|
+
multiSelect: question.multiSelect === true,
|
|
2778
|
+
options: options.flatMap((option) => isRecord(option) && typeof option.label === "string" ? [{ id: `${question.id}:${option.label}`, label: option.label, ...typeof option.description === "string" ? { description: option.description } : {} }] : [])
|
|
2779
|
+
}];
|
|
2780
|
+
})
|
|
2781
|
+
}
|
|
2782
|
+
}));
|
|
2783
|
+
}
|
|
2784
|
+
continue;
|
|
2785
|
+
}
|
|
2786
|
+
if (source.type === "deepseek-input-resolved") {
|
|
2787
|
+
const requestId = typeof source.payload.requestId === "string" ? source.payload.requestId : "";
|
|
2788
|
+
const existing = messages.find((message) => message.type === "tool_call" && message.inputRequest?.requestId === requestId);
|
|
2789
|
+
if (existing?.type === "tool_call" && existing.inputRequest) {
|
|
2790
|
+
existing.inputRequest.status = source.payload.status === "aborted" ? "aborted" : "resolved";
|
|
2791
|
+
existing.status = source.payload.status === "aborted" ? "failed" : "completed";
|
|
2792
|
+
}
|
|
2793
|
+
continue;
|
|
2794
|
+
}
|
|
2795
|
+
if (source.type === "deepseek-jobs" && Array.isArray(source.payload.jobs)) {
|
|
2796
|
+
for (const job of source.payload.jobs) {
|
|
2797
|
+
if (!isRecord(job) || typeof job.id !== "string") continue;
|
|
2798
|
+
const existing = messages.find((message) => message.type === "background_task" && message.taskId === job.id);
|
|
2799
|
+
const status = normalizeBackgroundTaskStatus(job.status);
|
|
2800
|
+
const next = {
|
|
2801
|
+
id: existing?.id ?? `deepseek-job-${job.id}`,
|
|
2802
|
+
type: "background_task",
|
|
2803
|
+
taskId: job.id,
|
|
2804
|
+
status,
|
|
2805
|
+
description: typeof job.label === "string" ? job.label : void 0,
|
|
2806
|
+
taskType: typeof job.kind === "string" ? job.kind : void 0,
|
|
2807
|
+
error: status === "failed" && typeof job.detail === "string" ? job.detail : void 0,
|
|
2808
|
+
timestamp: existing?.timestamp ?? source.timestamp
|
|
2809
|
+
};
|
|
2810
|
+
if (existing?.type === "background_task") Object.assign(existing, next);
|
|
2811
|
+
else messages.push(next);
|
|
2812
|
+
}
|
|
2813
|
+
continue;
|
|
2814
|
+
}
|
|
2815
|
+
if (source.type !== "deepseek-session-event" || !isRecord(source.payload.event)) continue;
|
|
2816
|
+
const event = source.payload.event;
|
|
2817
|
+
const data = isRecord(event.data) ? event.data : {};
|
|
2818
|
+
const seq = typeof event.seq === "number" ? event.seq : source.timestamp;
|
|
2819
|
+
if (event.type === "assistant/message") {
|
|
2820
|
+
const content = getDeepseekAssistantMessageText(event, "text");
|
|
2821
|
+
const reasoning = getDeepseekAssistantMessageText(event, "reasoning");
|
|
2822
|
+
if (reasoning.trim()) messages.push({ id: `deepseek-reasoning-${seq}`, type: "reasoning", content: reasoning, status: "completed", timestamp: source.timestamp });
|
|
2823
|
+
if (content.trim()) messages.push({ id: `deepseek-assistant-${seq}`, type: "agent", content, timestamp: source.timestamp });
|
|
2824
|
+
continue;
|
|
2825
|
+
}
|
|
2826
|
+
if (event.type === "tool/call") {
|
|
2827
|
+
const callId = typeof data.callId === "string" ? data.callId : String(seq);
|
|
2828
|
+
upsertDisplayMessage(messages, createCallDisplayMessage({
|
|
2829
|
+
id: `deepseek-tool-${callId}`,
|
|
2830
|
+
server: "deepseek",
|
|
2831
|
+
tool: typeof data.name === "string" ? data.name : "tool",
|
|
2832
|
+
input: parseArguments(data.arguments),
|
|
2833
|
+
status: "in_progress",
|
|
2834
|
+
timestamp: source.timestamp
|
|
2835
|
+
}));
|
|
2836
|
+
continue;
|
|
2837
|
+
}
|
|
2838
|
+
if (event.type === "tool/result") {
|
|
2839
|
+
const message = isRecord(data.message) ? data.message : {};
|
|
2840
|
+
const messageSource = isRecord(message.source) ? message.source : {};
|
|
2841
|
+
const result2 = Array.isArray(message.content) && isRecord(message.content[0]) ? message.content[0] : {};
|
|
2842
|
+
const callId = typeof messageSource.callId === "string" ? messageSource.callId : typeof data.callId === "string" ? data.callId : String(seq);
|
|
2843
|
+
const existing = messages.find((message2) => message2.id === `deepseek-tool-${callId}` && message2.type === "tool_call");
|
|
2844
|
+
if (existing?.type === "tool_call") {
|
|
2845
|
+
existing.output = stringifyDisplayValue(result2.content ?? data.result);
|
|
2846
|
+
existing.status = result2.isError === true || data.isError === true ? "failed" : "completed";
|
|
2847
|
+
}
|
|
2848
|
+
continue;
|
|
2849
|
+
}
|
|
2850
|
+
if (event.type === "todo/write" && Array.isArray(data.items)) {
|
|
2851
|
+
messages.push({
|
|
2852
|
+
id: `deepseek-todo-${seq}`,
|
|
2853
|
+
type: "todo_list",
|
|
2854
|
+
items: data.items.flatMap((item) => {
|
|
2855
|
+
if (!isRecord(item) || typeof item.content !== "string") return [];
|
|
2856
|
+
const itemStatus = item.status === "in_progress" || item.status === "completed" ? item.status : "pending";
|
|
2857
|
+
return [{ text: item.content, completed: itemStatus === "completed", itemStatus }];
|
|
2858
|
+
}),
|
|
2859
|
+
timestamp: source.timestamp
|
|
2860
|
+
});
|
|
2861
|
+
}
|
|
2862
|
+
}
|
|
2863
|
+
return messages;
|
|
2864
|
+
}
|
|
2865
|
+
|
|
2866
|
+
// ../shared/src/display-message/parsers/fx-parser.ts
|
|
2867
|
+
function getFxTextChunk(update, kind) {
|
|
2868
|
+
if (!isRecord(update) || update.sessionUpdate !== kind || !isRecord(update.content) || update.content.type !== "text") return null;
|
|
2869
|
+
return typeof update.content.text === "string" && update.content.text ? update.content.text : null;
|
|
2870
|
+
}
|
|
2871
|
+
function parseFxEvents(events) {
|
|
2872
|
+
const messages = [];
|
|
2873
|
+
for (const event of events) {
|
|
2874
|
+
const content = getUserMessage(event);
|
|
2875
|
+
if (content !== null) {
|
|
2876
|
+
if (content.trim()) {
|
|
2877
|
+
messages.push({
|
|
2878
|
+
id: getUserMessageId(event) ?? `fx-user-${event.timestamp}`,
|
|
2879
|
+
type: "user",
|
|
2880
|
+
content,
|
|
2881
|
+
timestamp: event.timestamp
|
|
2882
|
+
});
|
|
2883
|
+
}
|
|
2884
|
+
continue;
|
|
2885
|
+
}
|
|
2886
|
+
if (event.type === "fx-error") {
|
|
2887
|
+
messages.push({
|
|
2888
|
+
id: `fx-error-${event.timestamp}`,
|
|
2889
|
+
type: "error",
|
|
2890
|
+
message: typeof event.payload.message === "string" ? event.payload.message : "fx failed",
|
|
2891
|
+
timestamp: event.timestamp
|
|
2892
|
+
});
|
|
2893
|
+
continue;
|
|
2894
|
+
}
|
|
2895
|
+
if (event.type === "fx-turn-complete") {
|
|
2896
|
+
for (const message of messages) {
|
|
2897
|
+
if (message.type === "reasoning" && message.status === "in_progress") message.status = "completed";
|
|
2898
|
+
}
|
|
2899
|
+
continue;
|
|
2900
|
+
}
|
|
2901
|
+
if (event.type !== "fx-session-update" || !isRecord(event.payload.update)) continue;
|
|
2902
|
+
const update = event.payload.update;
|
|
2903
|
+
const kind = update.sessionUpdate;
|
|
2904
|
+
if (kind === "agent_message_chunk" || kind === "agent_thought_chunk") {
|
|
2905
|
+
const content2 = getFxTextChunk(update, kind);
|
|
2906
|
+
if (!content2) continue;
|
|
2907
|
+
const id = typeof update.messageId === "string" ? `fx-${update.messageId}` : `fx-${kind}-${event.timestamp}`;
|
|
2908
|
+
const existing = messages.find((message) => message.id === id);
|
|
2909
|
+
const text = `${existing && "content" in existing && typeof existing.content === "string" ? existing.content : ""}${content2}`;
|
|
2910
|
+
upsertDisplayMessage(messages, kind === "agent_thought_chunk" ? { id, type: "reasoning", content: text, status: "in_progress", timestamp: existing?.timestamp ?? event.timestamp } : { id, type: "agent", content: text, timestamp: existing?.timestamp ?? event.timestamp });
|
|
2911
|
+
continue;
|
|
2912
|
+
}
|
|
2913
|
+
if ((kind === "tool_call" || kind === "tool_call_update") && typeof update.toolCallId === "string") {
|
|
2914
|
+
const id = `fx-tool-${update.toolCallId}`;
|
|
2915
|
+
const existing = messages.find((message) => message.id === id && message.type === "tool_call");
|
|
2916
|
+
const status = update.status === "completed" || update.status === "failed" ? update.status === "failed" ? "failed" : "completed" : "in_progress";
|
|
2917
|
+
upsertDisplayMessage(messages, createCallDisplayMessage({
|
|
2918
|
+
id,
|
|
2919
|
+
server: "fx",
|
|
2920
|
+
tool: typeof update.name === "string" ? update.name : typeof update.title === "string" ? update.title : existing?.tool ?? "tool",
|
|
2921
|
+
input: update.rawInput === void 0 ? existing?.input : isRecord(update.rawInput) ? update.rawInput : stringifyDisplayValue(update.rawInput),
|
|
2922
|
+
output: update.rawOutput === void 0 ? existing?.output : stringifyDisplayValue(update.rawOutput),
|
|
2923
|
+
status,
|
|
2924
|
+
timestamp: event.timestamp
|
|
2925
|
+
}));
|
|
2060
2926
|
}
|
|
2061
2927
|
}
|
|
2062
2928
|
return messages;
|
|
@@ -2072,14 +2938,6 @@ function partFromEvent(event) {
|
|
|
2072
2938
|
function toolStatus(status) {
|
|
2073
2939
|
return status === "completed" ? "completed" : status === "error" ? "failed" : "in_progress";
|
|
2074
2940
|
}
|
|
2075
|
-
function setById(messages, id, next) {
|
|
2076
|
-
const index = messages.findIndex((message) => message.id === id);
|
|
2077
|
-
if (index === -1) {
|
|
2078
|
-
messages.push(next);
|
|
2079
|
-
} else {
|
|
2080
|
-
messages[index] = next;
|
|
2081
|
-
}
|
|
2082
|
-
}
|
|
2083
2941
|
function assistantError(info) {
|
|
2084
2942
|
if (!isRecord(info) || !isRecord(info.error)) return null;
|
|
2085
2943
|
const data = isRecord(info.error.data) ? info.error.data : null;
|
|
@@ -2093,8 +2951,9 @@ function parseOpencodeEvents(events) {
|
|
|
2093
2951
|
if (event.type === "event_msg" && event.payload.type === "user_message") {
|
|
2094
2952
|
const message = event.payload.message;
|
|
2095
2953
|
if (typeof message === "string" && message.trim()) {
|
|
2954
|
+
const messageId = event.payload[USER_MESSAGE_ID_PAYLOAD_KEY];
|
|
2096
2955
|
messages.push({
|
|
2097
|
-
id: `opencode-user-${event.timestamp}
|
|
2956
|
+
id: typeof messageId === "string" ? messageId : `opencode-user-${event.timestamp}`,
|
|
2098
2957
|
type: "user",
|
|
2099
2958
|
content: message,
|
|
2100
2959
|
timestamp: event.timestamp
|
|
@@ -2106,7 +2965,7 @@ function parseOpencodeEvents(events) {
|
|
|
2106
2965
|
const error = assistantError(event.payload.info);
|
|
2107
2966
|
if (error) {
|
|
2108
2967
|
messages.push({
|
|
2109
|
-
id: `opencode
|
|
2968
|
+
id: `opencode-${event.type}-${event.timestamp}`,
|
|
2110
2969
|
type: "error",
|
|
2111
2970
|
message: error,
|
|
2112
2971
|
timestamp: event.timestamp
|
|
@@ -2117,22 +2976,39 @@ function parseOpencodeEvents(events) {
|
|
|
2117
2976
|
if (event.type === "opencode-error") {
|
|
2118
2977
|
const message = typeof event.payload.message === "string" ? event.payload.message : "Opencode run failed";
|
|
2119
2978
|
messages.push({
|
|
2120
|
-
id: `opencode
|
|
2979
|
+
id: `opencode-${event.type}-${event.timestamp}`,
|
|
2121
2980
|
type: "error",
|
|
2122
2981
|
message,
|
|
2123
2982
|
timestamp: event.timestamp
|
|
2124
2983
|
});
|
|
2125
2984
|
continue;
|
|
2126
2985
|
}
|
|
2986
|
+
if (event.type === "opencode-session.next.tool.called") {
|
|
2987
|
+
const tool = typeof event.payload.tool === "string" ? event.payload.tool : "tool";
|
|
2988
|
+
const input = event.payload.input ?? event.payload.arguments;
|
|
2989
|
+
const callId = typeof event.payload.callID === "string" ? event.payload.callID : null;
|
|
2990
|
+
const id2 = callId ? `opencode-${callId}` : `opencode-${event.type}-${event.timestamp}`;
|
|
2991
|
+
upsertDisplayMessage(messages, createCallDisplayMessage({
|
|
2992
|
+
id: id2,
|
|
2993
|
+
server: "opencode",
|
|
2994
|
+
tool,
|
|
2995
|
+
input: isRecord(input) ? input : stringifyDisplayValue(input),
|
|
2996
|
+
status: "in_progress",
|
|
2997
|
+
timestamp: event.timestamp
|
|
2998
|
+
}));
|
|
2999
|
+
continue;
|
|
3000
|
+
}
|
|
2127
3001
|
const part = partFromEvent(event);
|
|
2128
3002
|
if (!part) continue;
|
|
2129
|
-
|
|
3003
|
+
let id = `opencode-${event.type}-${event.timestamp}`;
|
|
3004
|
+
if (typeof part.callID === "string") id = `opencode-${part.callID}`;
|
|
3005
|
+
else if (typeof part.id === "string") id = `opencode-${part.id}`;
|
|
2130
3006
|
const time = isRecord(part.time) ? part.time : null;
|
|
2131
3007
|
const timestamp = timestampFromMs(time?.start, event.timestamp);
|
|
2132
3008
|
if (part.type === "text") {
|
|
2133
3009
|
const text = typeof part.text === "string" ? part.text : "";
|
|
2134
3010
|
if (text.trim()) {
|
|
2135
|
-
|
|
3011
|
+
upsertDisplayMessage(messages, {
|
|
2136
3012
|
id,
|
|
2137
3013
|
type: "agent",
|
|
2138
3014
|
content: text,
|
|
@@ -2144,7 +3020,7 @@ function parseOpencodeEvents(events) {
|
|
|
2144
3020
|
if (part.type === "reasoning") {
|
|
2145
3021
|
const text = typeof part.text === "string" ? part.text : "";
|
|
2146
3022
|
if (text.trim()) {
|
|
2147
|
-
|
|
3023
|
+
upsertDisplayMessage(messages, {
|
|
2148
3024
|
id,
|
|
2149
3025
|
type: "reasoning",
|
|
2150
3026
|
content: text,
|
|
@@ -2157,23 +3033,24 @@ function parseOpencodeEvents(events) {
|
|
|
2157
3033
|
if (part.type === "tool" && isRecord(part.state)) {
|
|
2158
3034
|
const state = part.state;
|
|
2159
3035
|
const status = toolStatus(state.status);
|
|
3036
|
+
const tool = typeof part.tool === "string" ? part.tool : "tool";
|
|
3037
|
+
const input = state.input;
|
|
2160
3038
|
const output = state.status === "completed" ? stringifyDisplayValue(state.output) : state.status === "error" ? stringifyDisplayValue(state.error) : void 0;
|
|
2161
|
-
|
|
3039
|
+
upsertDisplayMessage(messages, createCallDisplayMessage({
|
|
2162
3040
|
id,
|
|
2163
|
-
type: "tool_call",
|
|
2164
3041
|
server: "opencode",
|
|
2165
|
-
tool
|
|
2166
|
-
input: isRecord(
|
|
3042
|
+
tool,
|
|
3043
|
+
input: isRecord(input) ? input : stringifyDisplayValue(input),
|
|
2167
3044
|
output,
|
|
2168
3045
|
status,
|
|
2169
3046
|
timestamp
|
|
2170
|
-
});
|
|
3047
|
+
}));
|
|
2171
3048
|
continue;
|
|
2172
3049
|
}
|
|
2173
3050
|
if (part.type === "patch") {
|
|
2174
3051
|
const files = Array.isArray(part.files) ? part.files.filter((file) => typeof file === "string") : [];
|
|
2175
3052
|
if (files.length > 0) {
|
|
2176
|
-
|
|
3053
|
+
upsertDisplayMessage(messages, {
|
|
2177
3054
|
id,
|
|
2178
3055
|
type: "file_change",
|
|
2179
3056
|
changes: files.map((path) => ({ path, kind: "update" })),
|
|
@@ -2186,193 +3063,83 @@ function parseOpencodeEvents(events) {
|
|
|
2186
3063
|
return messages;
|
|
2187
3064
|
}
|
|
2188
3065
|
|
|
2189
|
-
// ../shared/src/display-message/
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
"task_progress",
|
|
2193
|
-
"task_updated",
|
|
2194
|
-
"task_notification"
|
|
2195
|
-
]);
|
|
2196
|
-
function isBackgroundTaskSubtype(subtype) {
|
|
2197
|
-
return typeof subtype === "string" && BACKGROUND_TASK_SUBTYPES.has(subtype);
|
|
2198
|
-
}
|
|
2199
|
-
function optionalString(value) {
|
|
2200
|
-
return typeof value === "string" ? value : void 0;
|
|
2201
|
-
}
|
|
2202
|
-
function coerceBackgroundTaskPayload(payload) {
|
|
2203
|
-
if (!isRecord(payload)) return null;
|
|
2204
|
-
const subtype = payload.subtype;
|
|
2205
|
-
if (!isBackgroundTaskSubtype(subtype)) return null;
|
|
2206
|
-
const taskId = payload.task_id;
|
|
2207
|
-
if (typeof taskId !== "string" || !taskId) return null;
|
|
2208
|
-
const patch = isRecord(payload.patch) ? {
|
|
2209
|
-
status: optionalString(payload.patch.status),
|
|
2210
|
-
description: optionalString(payload.patch.description),
|
|
2211
|
-
error: optionalString(payload.patch.error),
|
|
2212
|
-
isBackgrounded: optionalBoolean(payload.patch.is_backgrounded)
|
|
2213
|
-
} : void 0;
|
|
2214
|
-
const usage = isRecord(payload.usage) ? {
|
|
2215
|
-
totalTokens: optionalNumber(payload.usage.total_tokens),
|
|
2216
|
-
toolUses: optionalNumber(payload.usage.tool_uses),
|
|
2217
|
-
durationMs: optionalNumber(payload.usage.duration_ms)
|
|
2218
|
-
} : void 0;
|
|
2219
|
-
return {
|
|
2220
|
-
subtype,
|
|
2221
|
-
taskId,
|
|
2222
|
-
description: optionalString(payload.description),
|
|
2223
|
-
summary: optionalString(payload.summary),
|
|
2224
|
-
outputFile: optionalString(payload.output_file),
|
|
2225
|
-
status: optionalString(payload.status),
|
|
2226
|
-
taskType: optionalString(payload.task_type),
|
|
2227
|
-
workflowName: optionalString(payload.workflow_name),
|
|
2228
|
-
prompt: optionalString(payload.prompt),
|
|
2229
|
-
lastToolName: optionalString(payload.last_tool_name),
|
|
2230
|
-
usage,
|
|
2231
|
-
patch
|
|
2232
|
-
};
|
|
2233
|
-
}
|
|
2234
|
-
function optionalNumber(value) {
|
|
2235
|
-
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
2236
|
-
}
|
|
2237
|
-
function optionalBoolean(value) {
|
|
2238
|
-
return typeof value === "boolean" ? value : void 0;
|
|
2239
|
-
}
|
|
2240
|
-
function normalizeBackgroundTaskStatus(status) {
|
|
2241
|
-
if (status === "completed" || status === "failed" || status === "stopped") {
|
|
2242
|
-
return status;
|
|
2243
|
-
}
|
|
2244
|
-
if (status === "killed") {
|
|
2245
|
-
return "stopped";
|
|
2246
|
-
}
|
|
2247
|
-
return "in_progress";
|
|
2248
|
-
}
|
|
2249
|
-
|
|
2250
|
-
// ../shared/src/display-message/task-accumulator.ts
|
|
2251
|
-
function mapTaskStatus(status) {
|
|
2252
|
-
if (status === "in_progress" || status === "completed") return status;
|
|
2253
|
-
return "pending";
|
|
2254
|
-
}
|
|
2255
|
-
function toAccumulatorInput(input) {
|
|
2256
|
-
if (typeof input === "string") {
|
|
2257
|
-
try {
|
|
2258
|
-
const parsed = JSON.parse(input);
|
|
2259
|
-
return isRecord(parsed) ? parsed : {};
|
|
2260
|
-
} catch {
|
|
2261
|
-
return {};
|
|
2262
|
-
}
|
|
2263
|
-
}
|
|
2264
|
-
return isRecord(input) ? input : {};
|
|
2265
|
-
}
|
|
2266
|
-
function extractToolResultText(content, separator = "\n") {
|
|
2267
|
-
if (typeof content === "string") return content;
|
|
2268
|
-
if (!Array.isArray(content)) return "";
|
|
2269
|
-
const texts = [];
|
|
2270
|
-
for (const part of content) {
|
|
2271
|
-
if (isRecord(part) && part.type === "text" && typeof part.text === "string") {
|
|
2272
|
-
texts.push(part.text);
|
|
2273
|
-
}
|
|
2274
|
-
}
|
|
2275
|
-
return texts.join(separator);
|
|
2276
|
-
}
|
|
2277
|
-
function normalizeContentBlocks(content) {
|
|
2278
|
-
if (typeof content === "string") {
|
|
2279
|
-
return content ? [{ type: "text", text: content }] : [];
|
|
2280
|
-
}
|
|
2281
|
-
return content ?? [];
|
|
3066
|
+
// ../shared/src/display-message/parsers/pi-parser.ts
|
|
3067
|
+
function nestedPayload(event) {
|
|
3068
|
+
return isRecord(event.payload.assistantMessageEvent) ? event.payload.assistantMessageEvent : event.payload;
|
|
2282
3069
|
}
|
|
2283
|
-
function
|
|
2284
|
-
|
|
2285
|
-
const trimmed = value.trim();
|
|
2286
|
-
return trimmed ? trimmed : null;
|
|
3070
|
+
function toolStatus2(value) {
|
|
3071
|
+
return value === "completed" || value === "success" ? "completed" : value === "error" ? "failed" : "in_progress";
|
|
2287
3072
|
}
|
|
2288
|
-
function
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
if (isRecord(parsed) && isRecord(parsed.task)) {
|
|
2292
|
-
return parseStringField(parsed.task.id);
|
|
2293
|
-
}
|
|
2294
|
-
} catch {
|
|
2295
|
-
}
|
|
2296
|
-
return resultText.match(/\bTask\s+#?([A-Za-z0-9_-]+)\s+created\b/i)?.[1] ?? null;
|
|
3073
|
+
function assistantErrorMessage(value) {
|
|
3074
|
+
if (!isRecord(value) || value.role !== "assistant" || value.stopReason !== "error") return null;
|
|
3075
|
+
return typeof value.errorMessage === "string" && value.errorMessage ? value.errorMessage : "Pi run failed";
|
|
2297
3076
|
}
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
if (!this.tasksByTaskId.delete(taskId)) return false;
|
|
2315
|
-
const idx = this.orderedKeys.indexOf(taskId);
|
|
2316
|
-
if (idx !== -1) this.orderedKeys.splice(idx, 1);
|
|
2317
|
-
return true;
|
|
3077
|
+
function parsePiEvents(events) {
|
|
3078
|
+
const messages = [];
|
|
3079
|
+
const text = /* @__PURE__ */ new Map();
|
|
3080
|
+
const thinking = /* @__PURE__ */ new Map();
|
|
3081
|
+
let activeMessageId = "assistant";
|
|
3082
|
+
for (const event of events) {
|
|
3083
|
+
if (event.type === "event_msg" && event.payload.type === "user_message" && typeof event.payload.message === "string") {
|
|
3084
|
+
messages.push({ id: `pi-user-${event.timestamp}-${messages.length}`, type: "user", content: event.payload.message, timestamp: event.timestamp });
|
|
3085
|
+
if (typeof event.payload.skillName === "string" && event.payload.skillName) {
|
|
3086
|
+
messages.push({
|
|
3087
|
+
id: `pi-skill-${event.timestamp}-${messages.length}`,
|
|
3088
|
+
type: "skill",
|
|
3089
|
+
skillName: event.payload.skillName,
|
|
3090
|
+
status: "completed",
|
|
3091
|
+
timestamp: event.timestamp
|
|
3092
|
+
});
|
|
2318
3093
|
}
|
|
2319
|
-
|
|
2320
|
-
const subject = parseStringField(input.subject) ?? existing?.subject;
|
|
2321
|
-
if (!subject) return false;
|
|
2322
|
-
const status = input.status !== void 0 ? mapTaskStatus(input.status) : existing?.status ?? "pending";
|
|
2323
|
-
this.tasksByTaskId.set(taskId, { subject, status });
|
|
2324
|
-
if (!existing) this.orderedKeys.push(taskId);
|
|
2325
|
-
return true;
|
|
3094
|
+
continue;
|
|
2326
3095
|
}
|
|
2327
|
-
if (
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
const existing = this.tasksByTaskId.get(id);
|
|
2336
|
-
const next = { subject, status: mapTaskStatus(raw.status) };
|
|
2337
|
-
if (existing && existing.subject === next.subject && existing.status === next.status) continue;
|
|
2338
|
-
this.tasksByTaskId.set(id, next);
|
|
2339
|
-
if (!existing) this.orderedKeys.push(id);
|
|
2340
|
-
changed = true;
|
|
3096
|
+
if (event.type === "pi-error") {
|
|
3097
|
+
messages.push({ id: `pi-error-${event.timestamp}-${messages.length}`, type: "error", message: String(event.payload.message ?? "Pi run failed"), timestamp: event.timestamp });
|
|
3098
|
+
continue;
|
|
3099
|
+
}
|
|
3100
|
+
if (event.type === "pi-message_end") {
|
|
3101
|
+
const error = assistantErrorMessage(event.payload.message);
|
|
3102
|
+
if (error) {
|
|
3103
|
+
messages.push({ id: `pi-error-${event.timestamp}-${messages.length}`, type: "error", message: error, timestamp: event.timestamp });
|
|
2341
3104
|
}
|
|
2342
|
-
|
|
3105
|
+
continue;
|
|
2343
3106
|
}
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
if (idx !== -1) this.orderedKeys.splice(idx, 1, taskId);
|
|
2355
|
-
return true;
|
|
2356
|
-
}
|
|
2357
|
-
getTasks() {
|
|
2358
|
-
const out = [];
|
|
2359
|
-
for (const key of this.orderedKeys) {
|
|
2360
|
-
const task = this.tasksByTaskId.get(key) ?? this.tasksByCreateToolUseId.get(key);
|
|
2361
|
-
if (task && task.subject) out.push(task);
|
|
3107
|
+
if (event.type === "pi-message_update") {
|
|
3108
|
+
const payload = nestedPayload(event);
|
|
3109
|
+
const id = typeof payload.id === "string" ? payload.id : activeMessageId;
|
|
3110
|
+
if (payload.type === "text_delta" && typeof payload.delta === "string") text.set(id, `${text.get(id) ?? ""}${payload.delta}`);
|
|
3111
|
+
if (payload.type === "thinking_delta" && typeof payload.delta === "string") thinking.set(id, `${thinking.get(id) ?? ""}${payload.delta}`);
|
|
3112
|
+
const textContent = text.get(id);
|
|
3113
|
+
const thinkingContent = thinking.get(id);
|
|
3114
|
+
if (textContent !== void 0) upsertDisplayMessage(messages, { id: `pi-${id}`, type: "agent", content: textContent, timestamp: event.timestamp });
|
|
3115
|
+
if (thinkingContent !== void 0) upsertDisplayMessage(messages, { id: `pi-thinking-${id}`, type: "reasoning", content: thinkingContent, status: "in_progress", timestamp: event.timestamp });
|
|
3116
|
+
continue;
|
|
2362
3117
|
}
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
if (block.type === "tool_use" && typeof block.name === "string" && typeof block.id === "string") {
|
|
2368
|
-
return this.processToolUse(block.name, block.id, toAccumulatorInput(block.input));
|
|
3118
|
+
if (event.type === "pi-message_start") {
|
|
3119
|
+
const message = isRecord(event.payload.message) ? event.payload.message : null;
|
|
3120
|
+
activeMessageId = typeof message?.id === "string" ? message.id : `assistant-${event.timestamp}`;
|
|
3121
|
+
continue;
|
|
2369
3122
|
}
|
|
2370
|
-
if (
|
|
2371
|
-
|
|
3123
|
+
if (event.type === "pi-tool_execution_start" || event.type === "pi-tool_execution_update" || event.type === "pi-tool_execution_end") {
|
|
3124
|
+
const payload = event.payload;
|
|
3125
|
+
const input = payload.args ?? payload.input;
|
|
3126
|
+
const id = typeof payload.toolCallId === "string" ? payload.toolCallId : typeof payload.id === "string" ? payload.id : `tool-${event.timestamp}`;
|
|
3127
|
+
const tool = typeof payload.toolName === "string" ? payload.toolName : "tool";
|
|
3128
|
+
const messageId = `pi-tool-${id}`;
|
|
3129
|
+
const status = event.type === "pi-tool_execution_end" ? toolStatus2(payload.isError ? "error" : "completed") : "in_progress";
|
|
3130
|
+
upsertDisplayMessage(messages, createCallDisplayMessage({
|
|
3131
|
+
id: messageId,
|
|
3132
|
+
server: "pi",
|
|
3133
|
+
tool,
|
|
3134
|
+
input: isRecord(input) ? input : stringifyDisplayValue(input),
|
|
3135
|
+
output: event.type === "pi-tool_execution_end" ? stringifyDisplayValue(payload.result ?? payload.error) : void 0,
|
|
3136
|
+
status,
|
|
3137
|
+
timestamp: event.timestamp
|
|
3138
|
+
}));
|
|
2372
3139
|
}
|
|
2373
|
-
return false;
|
|
2374
3140
|
}
|
|
2375
|
-
|
|
3141
|
+
return messages;
|
|
3142
|
+
}
|
|
2376
3143
|
|
|
2377
3144
|
// ../shared/src/display-message/parsers/mcp.ts
|
|
2378
3145
|
function parseMcpToolName(name) {
|
|
@@ -2388,13 +3155,22 @@ function parseMcpToolName(name) {
|
|
|
2388
3155
|
}
|
|
2389
3156
|
|
|
2390
3157
|
// ../shared/src/display-message/parsers/claude-parser.ts
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
3158
|
+
var ABORTED_TERMINAL_REASONS = /* @__PURE__ */ new Set(["aborted_streaming", "aborted_tools"]);
|
|
3159
|
+
function coerceClaudeResultPayload(payload) {
|
|
3160
|
+
return {
|
|
3161
|
+
is_error: typeof payload.is_error === "boolean" ? payload.is_error : void 0,
|
|
3162
|
+
subtype: typeof payload.subtype === "string" ? payload.subtype : void 0,
|
|
3163
|
+
errors: Array.isArray(payload.errors) ? payload.errors.filter((e) => typeof e === "string") : void 0,
|
|
3164
|
+
terminal_reason: typeof payload.terminal_reason === "string" ? payload.terminal_reason : void 0
|
|
3165
|
+
};
|
|
3166
|
+
}
|
|
3167
|
+
function stripAgentDiagnosticErrors(errors) {
|
|
3168
|
+
return errors.filter((error) => !error.startsWith("[ede_diagnostic]"));
|
|
3169
|
+
}
|
|
3170
|
+
function isClaudeResultError(payload) {
|
|
3171
|
+
if (payload.terminal_reason && ABORTED_TERMINAL_REASONS.has(payload.terminal_reason)) return false;
|
|
3172
|
+
if (payload.errors?.length && stripAgentDiagnosticErrors(payload.errors).length === 0) return false;
|
|
3173
|
+
return Boolean(payload.is_error) || payload.subtype !== "success";
|
|
2398
3174
|
}
|
|
2399
3175
|
var LOCAL_COMMAND_ECHO_REGEX = /^<(?:command-name|command-message|local-command-stdout|local-command-stderr)>/;
|
|
2400
3176
|
function parseClaudeEvents(events, parentToolUseId) {
|
|
@@ -2412,6 +3188,14 @@ function parseClaudeEvents(events, parentToolUseId) {
|
|
|
2412
3188
|
let liveStreamId = null;
|
|
2413
3189
|
const assistantThinking = /* @__PURE__ */ new Map();
|
|
2414
3190
|
const assistantTextCounts = /* @__PURE__ */ new Map();
|
|
3191
|
+
const acceptedUserMessageIndexes = /* @__PURE__ */ new Set();
|
|
3192
|
+
const canonicalUserMessageIndexes = /* @__PURE__ */ new Set();
|
|
3193
|
+
const findMatchingUserMessageIndex = (indexes, content, timestamp) => {
|
|
3194
|
+
return [...indexes].find((index) => {
|
|
3195
|
+
const message = messages[index];
|
|
3196
|
+
return message?.type === "user" && areUserMessagesWithinMatchWindow(message, { content, timestamp });
|
|
3197
|
+
});
|
|
3198
|
+
};
|
|
2415
3199
|
const taskAccumulator = new TaskAccumulator();
|
|
2416
3200
|
const taskSnapshot = () => taskAccumulator.getTasks().map((task) => ({
|
|
2417
3201
|
text: task.subject,
|
|
@@ -2419,6 +3203,29 @@ function parseClaudeEvents(events, parentToolUseId) {
|
|
|
2419
3203
|
itemStatus: task.status
|
|
2420
3204
|
}));
|
|
2421
3205
|
filteredEvents.forEach((event) => {
|
|
3206
|
+
if (event.type === "event_msg" && event.payload.type === "user_message") {
|
|
3207
|
+
const content = typeof event.payload.message === "string" ? event.payload.message : "";
|
|
3208
|
+
const images = userMessageImages(event.payload.images);
|
|
3209
|
+
if (content || images) {
|
|
3210
|
+
const messageId = event.payload[USER_MESSAGE_ID_PAYLOAD_KEY];
|
|
3211
|
+
const canonicalIndex = findMatchingUserMessageIndex(canonicalUserMessageIndexes, content, event.timestamp);
|
|
3212
|
+
const canonical = canonicalIndex === void 0 ? void 0 : messages[canonicalIndex];
|
|
3213
|
+
const message = {
|
|
3214
|
+
id: typeof messageId === "string" ? messageId : `user-${event.timestamp}`,
|
|
3215
|
+
type: "user",
|
|
3216
|
+
content,
|
|
3217
|
+
images: images?.length ? images : canonical?.type === "user" ? canonical.images : void 0,
|
|
3218
|
+
timestamp: event.timestamp
|
|
3219
|
+
};
|
|
3220
|
+
if (canonicalIndex === void 0) {
|
|
3221
|
+
acceptedUserMessageIndexes.add(messages.push(message) - 1);
|
|
3222
|
+
} else {
|
|
3223
|
+
messages[canonicalIndex] = message;
|
|
3224
|
+
canonicalUserMessageIndexes.delete(canonicalIndex);
|
|
3225
|
+
}
|
|
3226
|
+
}
|
|
3227
|
+
return;
|
|
3228
|
+
}
|
|
2422
3229
|
if (event.type === CLAUDE_PARTIAL_MESSAGE_EVENT_TYPE) {
|
|
2423
3230
|
const payload = coerceClaudePartialMessagePayload(event.payload);
|
|
2424
3231
|
if (!payload) return;
|
|
@@ -2476,13 +3283,25 @@ function parseClaudeEvents(events, parentToolUseId) {
|
|
|
2476
3283
|
};
|
|
2477
3284
|
}).filter((img) => img.data);
|
|
2478
3285
|
if (textContent || images.length > 0) {
|
|
2479
|
-
|
|
2480
|
-
|
|
3286
|
+
const acceptedIndex = findMatchingUserMessageIndex(
|
|
3287
|
+
acceptedUserMessageIndexes,
|
|
3288
|
+
textContent,
|
|
3289
|
+
event.timestamp
|
|
3290
|
+
);
|
|
3291
|
+
const accepted = acceptedIndex === void 0 ? void 0 : messages[acceptedIndex];
|
|
3292
|
+
const message = {
|
|
3293
|
+
id: accepted?.id ?? `user-${event.timestamp}`,
|
|
2481
3294
|
type: "user",
|
|
2482
3295
|
content: textContent || (images.length > 0 ? `[${images.length} image${images.length > 1 ? "s" : ""} attached]` : ""),
|
|
2483
|
-
images: images.length > 0 ? images : void 0,
|
|
3296
|
+
images: images.length > 0 ? images : accepted?.type === "user" ? accepted.images : void 0,
|
|
2484
3297
|
timestamp: event.timestamp
|
|
2485
|
-
}
|
|
3298
|
+
};
|
|
3299
|
+
if (acceptedIndex === void 0) {
|
|
3300
|
+
canonicalUserMessageIndexes.add(messages.push(message) - 1);
|
|
3301
|
+
} else {
|
|
3302
|
+
messages[acceptedIndex] = message;
|
|
3303
|
+
acceptedUserMessageIndexes.delete(acceptedIndex);
|
|
3304
|
+
}
|
|
2486
3305
|
}
|
|
2487
3306
|
}
|
|
2488
3307
|
if (event.type === "claude-assistant") {
|
|
@@ -2565,10 +3384,12 @@ function parseClaudeEvents(events, parentToolUseId) {
|
|
|
2565
3384
|
} else if (toolName === "Bash" || toolName === "bash" || toolName === "shell") {
|
|
2566
3385
|
const inputObj = typeof toolInput === "string" ? safeJsonParse(toolInput, {}) : toolInput;
|
|
2567
3386
|
const command = inputObj.command || "";
|
|
3387
|
+
const skillNames = skillNamesFromCommand(command);
|
|
2568
3388
|
messages.push({
|
|
2569
3389
|
id: `command-${event.timestamp}-${messages.length}`,
|
|
2570
3390
|
type: "command",
|
|
2571
3391
|
command,
|
|
3392
|
+
...skillNames.length > 0 ? { skillNames } : {},
|
|
2572
3393
|
output: "",
|
|
2573
3394
|
status: "in_progress",
|
|
2574
3395
|
timestamp: event.timestamp
|
|
@@ -2635,7 +3456,7 @@ function parseClaudeEvents(events, parentToolUseId) {
|
|
|
2635
3456
|
} else if (toolName === "Skill") {
|
|
2636
3457
|
const inputObj = typeof toolInput === "string" ? safeJsonParse(toolInput, {}) : toolInput;
|
|
2637
3458
|
messages.push({
|
|
2638
|
-
id: `skill-${
|
|
3459
|
+
id: `skill-${toolUseId}`,
|
|
2639
3460
|
type: "skill",
|
|
2640
3461
|
skillName: inputObj.skill || "unknown",
|
|
2641
3462
|
args: inputObj.args,
|
|
@@ -2644,19 +3465,37 @@ function parseClaudeEvents(events, parentToolUseId) {
|
|
|
2644
3465
|
});
|
|
2645
3466
|
} else if (toolName === "mcp__relay-subagent-tools__spawn_agent") {
|
|
2646
3467
|
const inputObj = typeof toolInput === "string" ? safeJsonParse(toolInput, {}) : toolInput;
|
|
3468
|
+
const provider = typeof inputObj.provider === "string" ? inputObj.provider : "unknown";
|
|
3469
|
+
const prompt = typeof inputObj.prompt === "string" ? inputObj.prompt : "";
|
|
3470
|
+
const model = typeof inputObj.model === "string" ? inputObj.model : void 0;
|
|
3471
|
+
const title = typeof inputObj.title === "string" ? inputObj.title : void 0;
|
|
2647
3472
|
messages.push({
|
|
2648
3473
|
id: `subagent-${event.timestamp}-${messages.length}`,
|
|
2649
3474
|
type: "subagent",
|
|
2650
3475
|
toolUseId,
|
|
2651
|
-
description:
|
|
2652
|
-
prompt
|
|
2653
|
-
subagentType:
|
|
2654
|
-
model
|
|
3476
|
+
description: title || `Relay subagent (${provider})`,
|
|
3477
|
+
prompt,
|
|
3478
|
+
subagentType: provider,
|
|
3479
|
+
model,
|
|
3480
|
+
thinkingLevel: isValidThinkingLevel(inputObj.thinking_level) ? inputObj.thinking_level : void 0,
|
|
2655
3481
|
status: "in_progress",
|
|
2656
3482
|
nestedEvents: [],
|
|
2657
3483
|
isChatBased: true,
|
|
2658
3484
|
timestamp: event.timestamp
|
|
2659
3485
|
});
|
|
3486
|
+
} else if (toolName === "mcp__relay-subagent-tools__message_agent") {
|
|
3487
|
+
const inputObj = typeof toolInput === "string" ? safeJsonParse(toolInput, {}) : toolInput;
|
|
3488
|
+
messages.push({
|
|
3489
|
+
id: `subagent-followup-${event.timestamp}-${messages.length}`,
|
|
3490
|
+
type: "subagent_followup",
|
|
3491
|
+
toolUseId,
|
|
3492
|
+
chatId: typeof inputObj.chatId === "string" ? inputObj.chatId : "",
|
|
3493
|
+
message: typeof inputObj.message === "string" ? inputObj.message : "",
|
|
3494
|
+
model: typeof inputObj.model === "string" ? inputObj.model : void 0,
|
|
3495
|
+
thinkingLevel: isValidThinkingLevel(inputObj.thinking_level) ? inputObj.thinking_level : void 0,
|
|
3496
|
+
status: "in_progress",
|
|
3497
|
+
timestamp: event.timestamp
|
|
3498
|
+
});
|
|
2660
3499
|
} else if (toolName === "mcp__relay-subagent-tools__delete_agent") {
|
|
2661
3500
|
const inputObj = typeof toolInput === "string" ? safeJsonParse(toolInput, {}) : toolInput;
|
|
2662
3501
|
const mcp = parseMcpToolName(toolName);
|
|
@@ -2684,14 +3523,13 @@ function parseClaudeEvents(events, parentToolUseId) {
|
|
|
2684
3523
|
});
|
|
2685
3524
|
}
|
|
2686
3525
|
if (event.type === "claude-result") {
|
|
2687
|
-
const payload = event.payload;
|
|
2688
|
-
if (payload
|
|
2689
|
-
const
|
|
2690
|
-
const errorMessage = errorList.length > 0 ? errorList.join("\n") : "Claude session encountered an unexpected error.";
|
|
3526
|
+
const payload = coerceClaudeResultPayload(event.payload);
|
|
3527
|
+
if (isClaudeResultError(payload)) {
|
|
3528
|
+
const genuineErrors = stripAgentDiagnosticErrors(payload.errors ?? []);
|
|
2691
3529
|
messages.push({
|
|
2692
3530
|
id: `error-${event.timestamp}`,
|
|
2693
3531
|
type: "error",
|
|
2694
|
-
message:
|
|
3532
|
+
message: genuineErrors.length > 0 ? genuineErrors.join("\n") : "Claude session encountered an unexpected error.",
|
|
2695
3533
|
timestamp: event.timestamp
|
|
2696
3534
|
});
|
|
2697
3535
|
}
|
|
@@ -2800,10 +3638,21 @@ function parseClaudeEvents(events, parentToolUseId) {
|
|
|
2800
3638
|
message.chatId = parsed.chatId;
|
|
2801
3639
|
}
|
|
2802
3640
|
}
|
|
3641
|
+
} else if (message.type === "subagent_followup") {
|
|
3642
|
+
message.output = resultContent;
|
|
3643
|
+
message.status = status;
|
|
2803
3644
|
}
|
|
2804
3645
|
}
|
|
2805
3646
|
}
|
|
2806
3647
|
});
|
|
3648
|
+
for (const message of messages) {
|
|
3649
|
+
if (message.type !== "subagent_followup") continue;
|
|
3650
|
+
const target = messages.find((candidate) => candidate.type === "subagent" && candidate.chatId === message.chatId);
|
|
3651
|
+
if (target?.type === "subagent") {
|
|
3652
|
+
message.targetTitle = target.description;
|
|
3653
|
+
message.targetProvider = target.subagentType;
|
|
3654
|
+
}
|
|
3655
|
+
}
|
|
2807
3656
|
const staleIndexes = /* @__PURE__ */ new Set();
|
|
2808
3657
|
for (const streamId of [...completedStreamIds, ...supersededStreamIds]) {
|
|
2809
3658
|
const refs = partialIndexes.get(streamId);
|
|
@@ -2825,19 +3674,101 @@ function parseAgentEvents(events, agentType) {
|
|
|
2825
3674
|
if (agentType === "cursor") {
|
|
2826
3675
|
return parseCursorEvents(events);
|
|
2827
3676
|
}
|
|
3677
|
+
if (agentType === "deepseek") {
|
|
3678
|
+
return parseDeepseekEvents(events);
|
|
3679
|
+
}
|
|
3680
|
+
if (agentType === "fx") {
|
|
3681
|
+
return parseFxEvents(events);
|
|
3682
|
+
}
|
|
2828
3683
|
if (agentType === "opencode") {
|
|
2829
3684
|
return parseOpencodeEvents(events);
|
|
2830
3685
|
}
|
|
3686
|
+
if (agentType === "pi") {
|
|
3687
|
+
return parsePiEvents(events);
|
|
3688
|
+
}
|
|
2831
3689
|
return parseClaudeEvents(events);
|
|
2832
3690
|
}
|
|
2833
3691
|
|
|
3692
|
+
// ../shared/src/pricing.ts
|
|
3693
|
+
var AUTOMATED_SOURCES = ["api", "automation"];
|
|
3694
|
+
var AUTOMATED_SOURCES_SQL = AUTOMATED_SOURCES.map((s) => `'${s}'`).join(", ");
|
|
3695
|
+
var PLANS = {
|
|
3696
|
+
trial: {
|
|
3697
|
+
id: "trial",
|
|
3698
|
+
name: "Free Trial",
|
|
3699
|
+
monthlyPrice: 0,
|
|
3700
|
+
seatPriceCents: 0,
|
|
3701
|
+
features: [
|
|
3702
|
+
"14 days free, no credit card required",
|
|
3703
|
+
"Unlimited manual workspace minutes",
|
|
3704
|
+
"15,000 automated workspace minutes",
|
|
3705
|
+
"Unlimited repositories and environments",
|
|
3706
|
+
"Unlimited automations",
|
|
3707
|
+
"Warm pools and warm hooks",
|
|
3708
|
+
"API access"
|
|
3709
|
+
]
|
|
3710
|
+
},
|
|
3711
|
+
developer: {
|
|
3712
|
+
id: "developer",
|
|
3713
|
+
name: "Developer",
|
|
3714
|
+
monthlyPrice: 120,
|
|
3715
|
+
seatPriceCents: 12e3,
|
|
3716
|
+
features: [
|
|
3717
|
+
"Unlimited manual workspace minutes",
|
|
3718
|
+
"5,000 automated workspace minutes per month",
|
|
3719
|
+
"Up to 10 repositories",
|
|
3720
|
+
"Up to 15 environments",
|
|
3721
|
+
"Up to 5 automations",
|
|
3722
|
+
"Warm pools and warm hooks",
|
|
3723
|
+
"API access"
|
|
3724
|
+
]
|
|
3725
|
+
},
|
|
3726
|
+
team: {
|
|
3727
|
+
id: "team",
|
|
3728
|
+
name: "Team",
|
|
3729
|
+
monthlyPrice: 300,
|
|
3730
|
+
seatPriceCents: 3e4,
|
|
3731
|
+
features: [
|
|
3732
|
+
"Unlimited manual workspace minutes",
|
|
3733
|
+
"15,000 automated workspace minutes per month",
|
|
3734
|
+
"Unlimited repositories",
|
|
3735
|
+
"Unlimited automations",
|
|
3736
|
+
"Warm pools and warm hooks",
|
|
3737
|
+
"Higher API rate limits",
|
|
3738
|
+
"Access to a static egress IP",
|
|
3739
|
+
"Auto-upgraded sandbox resources (4 vCPU, 32 GB disk, 16 GB memory)",
|
|
3740
|
+
"Shared Slack support channel"
|
|
3741
|
+
]
|
|
3742
|
+
},
|
|
3743
|
+
enterprise: {
|
|
3744
|
+
id: "enterprise",
|
|
3745
|
+
name: "Enterprise",
|
|
3746
|
+
monthlyPrice: 0,
|
|
3747
|
+
seatPriceCents: 0,
|
|
3748
|
+
features: [
|
|
3749
|
+
"Unlimited manual workspace minutes",
|
|
3750
|
+
"Unlimited automated workspace minutes",
|
|
3751
|
+
"Unlimited automations",
|
|
3752
|
+
"Custom warm hooks and pools",
|
|
3753
|
+
"Custom API rates",
|
|
3754
|
+
"Custom rate limits",
|
|
3755
|
+
"Shared Slack support channel",
|
|
3756
|
+
"SOC 2",
|
|
3757
|
+
"Data Processing Agreement (DPA)",
|
|
3758
|
+
"SCIM user provisioning",
|
|
3759
|
+
"Custom security and privacy settings"
|
|
3760
|
+
]
|
|
3761
|
+
}
|
|
3762
|
+
};
|
|
3763
|
+
var TEAM_PLAN = PLANS.team;
|
|
3764
|
+
var ENTERPRISE_PLAN = PLANS.enterprise;
|
|
3765
|
+
|
|
2834
3766
|
// ../shared/src/routes/workspaces.ts
|
|
2835
3767
|
var WORKSPACE_STATUSES = ["active", "sleeping", "archived", "preparing", "error"];
|
|
2836
3768
|
var VALID_LIFECYCLE_POLICIES = ["default", "archive_when_done", "sleep_when_done", "delete_after_inactivity"];
|
|
2837
|
-
var WORKSPACE_FILE_UPLOAD_MAX_SIZE_BYTES = 20 * 1024 * 1024;
|
|
2838
|
-
var WORKSPACE_FILE_CONTENT_MAX_SIZE_BYTES = 1 * 1024 * 1024;
|
|
2839
3769
|
var WORKSPACE_STATUS_FILTERS = WORKSPACE_STATUSES.map((status) => status === "error" ? "failed" : status);
|
|
2840
3770
|
var WORKSPACE_SORT_CHOICES = [
|
|
3771
|
+
["manual", "Custom"],
|
|
2841
3772
|
["activity", "Last activity"],
|
|
2842
3773
|
["created", "Created date"],
|
|
2843
3774
|
["status", "Status"]
|
|
@@ -2892,24 +3823,24 @@ function result(data, isError = false) {
|
|
|
2892
3823
|
}
|
|
2893
3824
|
var server = new McpServer({
|
|
2894
3825
|
name: "replicas-mcp",
|
|
2895
|
-
version: "0.2.
|
|
3826
|
+
version: "0.2.13"
|
|
2896
3827
|
});
|
|
2897
3828
|
var registerTool = server.registerTool.bind(server);
|
|
2898
|
-
var codingAgentSchema =
|
|
3829
|
+
var codingAgentSchema = z3.string().refine(isValidCodingAgentProvider, {
|
|
2899
3830
|
message: `Coding agent must be ${getCodingAgentDisplayNames()}`
|
|
2900
3831
|
});
|
|
2901
|
-
var lifecyclePolicySchema =
|
|
3832
|
+
var lifecyclePolicySchema = z3.enum(VALID_LIFECYCLE_POLICIES);
|
|
2902
3833
|
registerTool(
|
|
2903
3834
|
"create_replica",
|
|
2904
3835
|
{
|
|
2905
3836
|
description: "Create a new replica workspace and send initial message",
|
|
2906
3837
|
inputSchema: {
|
|
2907
|
-
name:
|
|
2908
|
-
message:
|
|
2909
|
-
repository_ids:
|
|
2910
|
-
repository_set_id:
|
|
3838
|
+
name: z3.string().regex(/^\S+$/, "Name cannot contain spaces"),
|
|
3839
|
+
message: z3.string(),
|
|
3840
|
+
repository_ids: z3.array(z3.string()).min(1).optional().describe("Repository IDs to include. Mutually exclusive with repository_set_id."),
|
|
3841
|
+
repository_set_id: z3.string().optional().describe("Repository set ID to use. Mutually exclusive with repository_ids."),
|
|
2911
3842
|
coding_agent: codingAgentSchema.optional().describe("Coding agent to use (default: claude)"),
|
|
2912
|
-
model:
|
|
3843
|
+
model: z3.string().optional().describe("Model to use for the coding agent"),
|
|
2913
3844
|
lifecycle_policy: lifecyclePolicySchema.optional().describe("Lifecycle policy for the workspace")
|
|
2914
3845
|
}
|
|
2915
3846
|
},
|
|
@@ -2935,11 +3866,11 @@ registerTool(
|
|
|
2935
3866
|
{
|
|
2936
3867
|
description: "List all replicas with pagination and optional filters",
|
|
2937
3868
|
inputSchema: {
|
|
2938
|
-
page:
|
|
2939
|
-
limit:
|
|
2940
|
-
user_id:
|
|
2941
|
-
source:
|
|
2942
|
-
status:
|
|
3869
|
+
page: z3.number().optional(),
|
|
3870
|
+
limit: z3.number().optional(),
|
|
3871
|
+
user_id: z3.string().optional().describe("Filter by creator user ID"),
|
|
3872
|
+
source: z3.enum(["api", "dashboard", "slack", "linear", "github", "gitlab", "automation"]).optional().describe("Filter by creation source"),
|
|
3873
|
+
status: z3.enum(["active", "sleeping", "preparing"]).optional().describe("Filter by workspace status")
|
|
2943
3874
|
}
|
|
2944
3875
|
},
|
|
2945
3876
|
async (args) => {
|
|
@@ -2962,8 +3893,8 @@ registerTool(
|
|
|
2962
3893
|
{
|
|
2963
3894
|
description: "Get replica details by ID, including current status, chats, and repository statuses",
|
|
2964
3895
|
inputSchema: {
|
|
2965
|
-
id:
|
|
2966
|
-
include:
|
|
3896
|
+
id: z3.string(),
|
|
3897
|
+
include: z3.array(z3.enum(["environment", "diffs"])).optional().describe("Extra data to include in response")
|
|
2967
3898
|
}
|
|
2968
3899
|
},
|
|
2969
3900
|
async (args) => {
|
|
@@ -2982,12 +3913,12 @@ registerTool(
|
|
|
2982
3913
|
{
|
|
2983
3914
|
description: "Send a message to an existing replica. Automatically handles waking sleeping replicas.",
|
|
2984
3915
|
inputSchema: {
|
|
2985
|
-
id:
|
|
2986
|
-
message:
|
|
3916
|
+
id: z3.string(),
|
|
3917
|
+
message: z3.string(),
|
|
2987
3918
|
coding_agent: codingAgentSchema.optional().describe("Coding agent to use"),
|
|
2988
|
-
chat_id:
|
|
2989
|
-
model:
|
|
2990
|
-
plan_mode:
|
|
3919
|
+
chat_id: z3.string().optional().describe("Chat ID to send the message to (uses default chat if omitted)"),
|
|
3920
|
+
model: z3.string().optional().describe("Model to use"),
|
|
3921
|
+
plan_mode: z3.boolean().optional().describe("Whether to use plan mode")
|
|
2991
3922
|
}
|
|
2992
3923
|
},
|
|
2993
3924
|
async (args) => {
|
|
@@ -3008,7 +3939,7 @@ registerTool(
|
|
|
3008
3939
|
{
|
|
3009
3940
|
description: "Delete a replica. This terminates the workspace and cannot be undone.",
|
|
3010
3941
|
inputSchema: {
|
|
3011
|
-
id:
|
|
3942
|
+
id: z3.string()
|
|
3012
3943
|
}
|
|
3013
3944
|
},
|
|
3014
3945
|
async (args) => {
|
|
@@ -3050,7 +3981,7 @@ registerTool(
|
|
|
3050
3981
|
{
|
|
3051
3982
|
description: "List chats in a replica workspace",
|
|
3052
3983
|
inputSchema: {
|
|
3053
|
-
id:
|
|
3984
|
+
id: z3.string().describe("Replica ID")
|
|
3054
3985
|
}
|
|
3055
3986
|
},
|
|
3056
3987
|
async (args) => {
|
|
@@ -3066,9 +3997,9 @@ registerTool(
|
|
|
3066
3997
|
{
|
|
3067
3998
|
description: "Create a new chat in a replica workspace",
|
|
3068
3999
|
inputSchema: {
|
|
3069
|
-
id:
|
|
4000
|
+
id: z3.string().describe("Replica ID"),
|
|
3070
4001
|
provider: codingAgentSchema.describe("Coding agent provider for the chat"),
|
|
3071
|
-
title:
|
|
4002
|
+
title: z3.string().optional().describe("Optional title for the chat")
|
|
3072
4003
|
}
|
|
3073
4004
|
},
|
|
3074
4005
|
async (args) => {
|
|
@@ -3086,8 +4017,8 @@ registerTool(
|
|
|
3086
4017
|
{
|
|
3087
4018
|
description: "Delete a chat from a replica workspace",
|
|
3088
4019
|
inputSchema: {
|
|
3089
|
-
id:
|
|
3090
|
-
chat_id:
|
|
4020
|
+
id: z3.string().describe("Replica ID"),
|
|
4021
|
+
chat_id: z3.string().describe("Chat ID to delete")
|
|
3091
4022
|
}
|
|
3092
4023
|
},
|
|
3093
4024
|
async (args) => {
|
|
@@ -3103,8 +4034,8 @@ registerTool(
|
|
|
3103
4034
|
{
|
|
3104
4035
|
description: "Read conversation history of a replica. Returns parsed DisplayMessage objects (user messages, agent responses, commands, file changes, tool calls, etc). If the workspace is sleeping, it will be woken and you should retry in 30-90 seconds.",
|
|
3105
4036
|
inputSchema: {
|
|
3106
|
-
id:
|
|
3107
|
-
chat_id:
|
|
4037
|
+
id: z3.string(),
|
|
4038
|
+
chat_id: z3.string().optional().describe("Read history for a specific chat (uses default chat if omitted)")
|
|
3108
4039
|
}
|
|
3109
4040
|
},
|
|
3110
4041
|
async (args) => {
|
|
@@ -3138,7 +4069,7 @@ registerTool(
|
|
|
3138
4069
|
{
|
|
3139
4070
|
description: "Get warm and start hook logs for a replica workspace",
|
|
3140
4071
|
inputSchema: {
|
|
3141
|
-
id:
|
|
4072
|
+
id: z3.string().describe("Replica ID")
|
|
3142
4073
|
}
|
|
3143
4074
|
},
|
|
3144
4075
|
async (args) => {
|